寫在前面
這里只介紹liteflow的簡單基礎使用以及作者對liteflow進行可視化擴展的相關闡述
一、背景及意義
背景:對于擁有復雜業(yè)務邏輯的系統(tǒng)承載著核心業(yè)務邏輯,這些核心業(yè)務邏輯涉及內部邏輯運算,緩存操作,持久化操作,外部資源調取,內部其他系統(tǒng)RPC調用等等。項目幾經易手,維護的成本就會越來越高。各種硬代碼判斷,分支條件越來越多。代碼的抽象,復用率也越來越低,各個模塊之間的耦合度很高。一小段邏輯的變動,會影響到其他模塊,需要進行完整回歸測試來驗證。如要靈活改變業(yè)務流程的順序,則要進行代碼大改動進行抽象,重新寫方法。實時熱變更業(yè)務流程,幾乎很難實現(xiàn)
意義:邏輯解耦、提高擴展性、降低維護成本、能力充分復用、流程靈活編排
二、常用流程編排框架
liteflow(開源) | asyncTool(開源) | JDEasyFlow(開源) | disruptor | |
---|---|---|---|---|
介紹 | LiteFlow是一個非常強大的現(xiàn)代化的規(guī)則引擎框架,融合了編排特性和規(guī)則引擎的所有特性。如果你要對復雜業(yè)務邏輯進行新寫或者重構,用LiteFlow最合適不過。它是一個編排式的規(guī)則引擎框架,組件編排,幫助解耦業(yè)務代碼,讓每一個業(yè)務片段都是一個組件。 | 解決任意的多線程并行、串行、阻塞、依賴、回調的并發(fā)框架,可以任意組合各線程的執(zhí)行順序,帶全鏈路回調和超時控制。 | 通用流程編排技術組件,適用于服務編排、工作流、審批流等場景 | |
地址 | LiteFlow | asyncTool: 解決任意的多線程并行、串行、阻塞、依賴、回調的并行框架,可以任意組合各線程的執(zhí)行順序,帶全鏈路執(zhí)行結果回調。多線程編排一站式解決方案。來自于京東主App后臺。 | 流程編排、如此簡單-通用流程編排組件JDEasyFlow介紹-京東云開發(fā)者社區(qū) | |
優(yōu)點 | 復雜業(yè)務流程編排、社區(qū)成熟活躍 | 基于jdk8 CompletableFuture、輕量級 | 簡單、靈活、易擴展 | 基于生產-消費模型、無鎖設計 |
缺點 | 開源框架較重,有一定學習成本 | 新框架穩(wěn)定性待驗證 | 較為底層,針對業(yè)務場景需要二次封裝 | |
示例 | liteflow-example: liteflow的一個業(yè)務示例工程https://gitee.com/bryan31/liteFlow |
三、liteflow基礎使用
1.添加依賴jar包
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring</artifactId>
<version>2.10.4</version>
</dependency>
2.定義組件
定義組件和實現(xiàn)某些組件,注冊進上下文
@Component("a")
public class ACmp extends NodeComponent {
@Override
public void process() {
//do your business
}
}
3.配置
添加對應的配置類及配置文件
Spring xml中的配置
<context:component-scan base-package="com.yomahub.flowtest.components" />
<bean id="springAware" class="com.yomahub.liteflow.spi.spring.SpringAware"/>
<bean class="com.yomahub.liteflow.spring.ComponentScanner"/>
<bean id="liteflowConfig" class="com.yomahub.liteflow.property.LiteflowConfig">
<property name="ruleSource" value="config/flow.el.xml"/>
</bean>
<bean id="flowExecutor" class="com.yomahub.liteflow.core.FlowExecutor">
<property name="liteflowConfig" ref="liteflowConfig"/>
</bean>
<!-- 如果上述enableLog為false,下面這段也可以省略 -->
<bean class="com.yomahub.liteflow.monitor.MonitorBus">
<property name="liteflowConfig" ref="liteflowConfig"/>
</bean>
4.規(guī)則文件的定義
--流程的定義(第3步中l(wèi)iteflowConfig指定了規(guī)則文件為config/flow.xml),所以需要在resources下新建文件夾config,在新建flow.xml文件,配置要定義的流程
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<chain name="chain1">
THEN(a, b, c)
</chain>
</flow>
5.執(zhí)行
編排好的流程,在需要執(zhí)行的地方注入FlowExecutor,執(zhí)行execute2Resp
@Component
public class YourClass{
@Resource
private FlowExecutor flowExecutor;
@Test
public void testConfig(){
LiteflowResponse response = flowExecutor.execute2Resp("chain1", "arg");
}
}
四、liteflow在實際中的應用
這里弱化背后的實際業(yè)務只展示作者在實際中的應用案例
1.添加依賴jar包
<properties>
<liteflow-spring.version>2.8.0</liteflow-spring.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring</artifactId>
<version>${liteflow-spring.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
2.定義組件
定義組件和實現(xiàn)某些組件,注冊進上下文
@LiteflowComponent("checkRealNameAuthCmp")
@LiteflowCmpDefine
public class CheckRealNameAuthCmp {
private static final Logger log = LoggerFactory.getLogger(CheckRealNameAuthCmp.class);
@LiteflowMethod(LiteFlowMethodEnum.PROCESS)
public void process(NodeComponent nodeComponent) throws Exception {
// 獲取請求參數(shù)
GeneratePolicyRightsParam generatePolicyRightsParam = nodeComponent.getSlot().getRequestData();
// 如果pin為空則結束流程
if (generatePolicyRightsParam == null || StringUtil.isEmpty(generatePolicyRightsParam.getUserPin())) {
log.info("CheckRealNameAuthCmp -> process end, nodeComponent={},pin is null.", JsonUtil.toJSONString(nodeComponent));
nodeComponent.setIsEnd(true);
}
//封裝設置流程編排上下文信息
GenerateRightsContext generateRightsContext = nodeComponent.getContextBean(GenerateRightsContext.class);
generateRightsContext.setGeneratePolicyRightsParam(generatePolicyRightsParam);
}
}
LiteflowComponent:??普通組件 | LiteFlow
LiteflowCmpDefine:??聲明式組件 | LiteFlow
3.配置
添加對應的配置類及配置文件
Spring xml中的配置
spring-config.xml
<import resource="classpath*:spring/spring-config-liteflow.xml"/>
spring-config-liteflow.xml
<bean id="springAware" class="com.yomahub.liteflow.spi.spring.SpringAware"/>
<bean id="springComponentScaner" class="com.yomahub.liteflow.spring.ComponentScanner"/>
<!-- 注入liteflow的配置文件 -->
<bean id="liteflowConfig" class="com.yomahub.liteflow.property.LiteflowConfig">
<property name="ruleSource" value="liteflow/flow.xml"/>
</bean>
<!-- 注入liteflow的執(zhí)行引擎 -->
<bean id="flowExecutor" class="com.yomahub.liteflow.core.FlowExecutor">
<property name="liteflowConfig" ref="liteflowConfig"/>
</bean>
<!-- 注入liteflow的MonitorBus-->
<bean class="com.yomahub.liteflow.monitor.MonitorBus">
<constructor-arg ref="liteflowConfig"/>
<property name="liteflowConfig" ref="liteflowConfig"/>
</bean>
4.規(guī)則文件的定義
--流程的定義(第3步中l(wèi)iteflowConfig指定了規(guī)則文件為liteflow/flow.xml),所以需要在resources下新建文件夾liteflow,在新建flow.xml文件,配置要定義的流程
flow.xml
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<!-- liteflow流程編排:生成(發(fā)放)保單權益-->
<chain name="sendPolicyRightsChain">
<when value="checkRealNameAuthCmp"/>
<then value="checkNewPolicyRightsCmp"/>
<then value="getPolicyInfoCmp"/>
<then value="policyMatchServiceRuleCmp"/>
<then value="initPolicyRightsDataCmp"/>
<then value="creatPlanGantRightsDataCmp"/>
<then value="asyncFullFillCmp"/>
</chain>
</flow>
5.執(zhí)行
執(zhí)行編排好的流程,在需要執(zhí)行的地方注入FlowExecutor,執(zhí)行execute2Resp
@Resource
private FlowExecutor flowExecutor;
public Boolean sendPolicyRights(GeneratePolicyRightsParam generatePolicyRightsParam) {
//todo 入參和上下文不能混用,通用信息用map
LiteflowResponse response = flowExecutor.execute2Resp("sendPolicyRightsChain", generatePolicyRightsParam, GenerateRightsContext.class,GenerateRightsContext.class);
}
五、liteflow能力擴展(可視化)
liteflowt提供了流程編排的能力,只有研發(fā)人員能夠了解這內在的流程編排含義,對于其他產品或者業(yè)務并不能直觀的了解當前的業(yè)務流程,可視化并不友好。這時我們如何讓當前的流程可視化呢?編寫一個頁面直接讀取配置文件flow.xml進行顯示,這是沒有意義的。有意義的是我們能夠對組件進行可視化、對流程可視化、對流程編排可視化。
1、思想
提供新的jar包,獲取到業(yè)務系統(tǒng)聲名的組件、流程、顯示流程和組件、提供編排能力。
說明:
1、小工具jar包為可視化流程編排小工具,主要提供獲取業(yè)務系統(tǒng)聲明的組件、保存的流程、進行流程可視化展示、進行流程編排可視化等,使用liteflow-util標識區(qū)別于業(yè)務系統(tǒng)。
2、業(yè)務系統(tǒng)為組件聲明、流程執(zhí)行、業(yè)務邏輯系統(tǒng),使用liteflow-test標識
2、實現(xiàn)
2.1獲取特定的類或方法
如何從liteflow-util中獲取liteflow-test中聲明的組件
2.1.1獲取上下文環(huán)境
ApplicationContextAware
當一個bean的屬性初始化后會回調到setApplicationContext,從而設置應用上下文。
public interface ApplicationContextAware extends Aware {
/**
* Set the ApplicationContext that this object runs in.
* Normally this call will be used to initialize the object.
*
Invoked after population of normal bean properties but before an init callback such
* as {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet()}
* or a custom init-method. Invoked after {@link ResourceLoaderAware#setResourceLoader},
* {@link ApplicationEventPublisherAware#setApplicationEventPublisher} and
* {@link MessageSourceAware}, if applicable.
* @param applicationContext the ApplicationContext object to be used by this object
* @throws ApplicationContextException in case of context initialization errors
* @throws BeansException if thrown by application context methods
* @see org.springframework.beans.factory.BeanInitializationException
*/
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
在liteflow-util中使用一個類來實現(xiàn)ApplicationContextAware,從而獲取到liteflow-test(依賴當前jar包的應用)的上下文環(huán)境
@Configuration
public class LiteFlowApplicationContext implements ApplicationContextAware {
private static ApplicationContext controllerApplicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
System.out.println("applicationContext = " + applicationContext);
LiteFlowApplicationContext.controllerApplicationContext=applicationContext;
}
public static ApplicationContext getControllerApplicationContext() {
return controllerApplicationContext;
}
}
2.1.2從上下文獲取類
在liteflow-util中根據上下文環(huán)境獲取組件類這里的重點是Map<String, Object> mvcObjects = context.getBeansWithAnnotation(Service.class);
@Slf4j
public class ReferenceManager {
private static Map<Class<?>, Object> interfaceMapRef = new ConcurrentHashMap<Class<?>, Object>();
private static ReferenceManager instance;
private ReferenceManager() {
}
public synchronized static ReferenceManager getInstance() {
if (null != instance) {
return instance;
}
instance = new ReferenceManager();
ApplicationContext controllerContext = LiteFlowApplicationContext.getControllerApplicationContext();
interfaceMapInit(controllerContext);
return instance;
}
private static void interfaceMapInit(ApplicationContext context) {
try {
Map<String, Object> objects = Maps.newHashMapWithExpectedSize(64);
//優(yōu)化 允許 ServiceBean 被MVC容器掃描
Map<String, Object> mvcObjects = context.getBeansWithAnnotation(Service.class);
objects.putAll(mvcObjects);
if (objects == null || objects.size() == 0) {
return;
}
for (Entry<String, Object> entry : objects.entrySet()) {
/**
* 獲取代理對象的原對象
* 因為 jdk 動態(tài)代理通過接口
*/
Object objectImplProxy = entry.getValue();
Object objectImpl = AopTargetUtils.getTarget(objectImplProxy);
Class objectImplClass = objectImpl.getClass();
if (objectImplClass.getInterfaces().length > 0) {
/**
* 規(guī)定 每個interface 只對應 一個實現(xiàn)類
* 如果 多個類實現(xiàn)了該接口 接口列表中只 顯示第一個實現(xiàn)類
*/
Class interfaceClass = objectImplClass.getInterfaces()[0];
Object object = interfaceMapRef.get(interfaceClass);
if (object == null) {
interfaceMapRef.put(interfaceClass, objectImpl);
} else {
}
} else {
}
}
} catch (Exception e) {
}
}
public Map<Class<?>, Object> getInterfaceMapRef() {
return interfaceMapRef;
}
}
@Component
public class ServiceScanner {
public Set<Class<?>> classes() {
return interfaceMapRef().keySet();
}
public Map<Class<?>, Object> interfaceMapRef() {
return ReferenceManager.getInstance().getInterfaceMapRef();
}
}
public class AopTargetUtils {
/**
* 獲取 目標對象
* @param proxy 代理對象
* @return
* @throws Exception
*/
public static Object getTarget(Object proxy) throws Exception {
if(!AopUtils.isAopProxy(proxy)) {
return proxy;//不是代理對象
}
if(AopUtils.isJdkDynamicProxy(proxy)) {
return getJdkDynamicProxyTargetObject(proxy);
} else { //cglib
return getCglibProxyTargetObject(proxy);
}
}
private static Object getCglibProxyTargetObject(Object proxy) {
try{
Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
h.setAccessible(true);
Object dynamicAdvisedInterceptor = h.get(proxy);
Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
return target;
} catch(Exception e){
e.printStackTrace();
return null;
}
}
private static Object getJdkDynamicProxyTargetObject(Object proxy) {
try{
Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
AopProxy aopProxy = (AopProxy) h.get(proxy);
Field advised = aopProxy.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport) advised.get(aopProxy)).getTargetSource().getTarget();
return target;
} catch(Exception e){
e.printStackTrace();
return null;
}
}
}
2.2訪問liteflow-util頁面
如何在liteflow-test里訪問到liteflow-util包里的頁面并展示
(1)在liteflow-util內編寫一個Servlet類,直接繼承HttpServlet ,重寫doGet或者doPost方法
(2)在liteflow-util內將Servlet類配置到web.xml中
(3)在liteflow-util內準備前端的頁面(form表單、按鈕交互)
(4)在liteflow-test內引入依賴并啟動liteflow-test文章來源:http://www.zghlxwxcb.cn/news/detail-829221.html
2.2.1HttpServlet
public class HandServlet extends HttpServlet {
private static final Logger log = LoggerFactory.getLogger(HandServlet.class);
private String username = null;
private String password = null;
private ServletContext servletContext;
public HandServlet() {
}
@Override
public void init(ServletConfig config) {
log.info("HandServlet->init,start");
this.username = config.getInitParameter("loginUsername");
this.password = config.getInitParameter("loginPassword");
this.servletContext = config.getServletContext();
log.info("HandServlet->init finish");
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
String contextPath = request.getContextPath();
String servletPath = request.getServletPath();
String requestURI = request.getRequestURI();
response.setCharacterEncoding("utf-8");
if (contextPath == null) {
contextPath = "";
}
String uri = contextPath + servletPath;
String path = requestURI.substring(contextPath.length() + servletPath.length());
String usernameParam;
if (!Objects.equals("/submitLogin", path)) {
if (this.needLogin(request, path)) {
this.redirect(request, response);
} else {
Result result;
try {
result = this.requestHandler(path, request);
} catch (Throwable var11) {
log.error("HandServlet->service,requestHandler error", var11);
result = Result.buildFail(var11.getMessage());
}
if (null != result) {
response.getWriter().print(JSON.toJSONString(result));
} else {
this.returnResourceFile(path, uri, response);
}
}
} else {
usernameParam = request.getParameter("loginUsername");
String passwordParam = request.getParameter("loginPassword");
System.out.println("usernameParam = " + usernameParam);
System.out.println("passwordParam = " + passwordParam);
// if (this.username.equals(usernameParam) && this.password.equals(passwordParam)) {
HttpSession session = request.getSession();
session.setAttribute("lite-flow", this.username);
session.setMaxInactiveInterval(300);
response.getWriter().print(JSON.toJSONString(Result.buildSuccess("success")));
// } else {
// response.getWriter().print(JSON.toJSONString(Result.buildFail("用戶名或密碼錯誤")));
// }
}
}
private void redirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (request.getHeader("X-Requested-With") != null && "XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
response.getWriter().print(JSON.toJSONString(Result.buildReLogin()));
} else if (request.getHeader("Accept") != null && request.getHeader("Accept").contains("application/json")) {
response.getWriter().print(JSON.toJSONString(Result.buildReLogin()));
} else {
response.sendRedirect("/lite-flow/login.html");
}
}
private Result requestHandler(String path, HttpServletRequest request) {
System.out.println("path = " + path);
System.out.println("request = " + request);
String initMenu = "/initMenu";
String liteflow = "/liteflow";
if (initMenu.equals(path)) {
Map<String, Object> map = new HashMap(2);
List<String> classObjectMap = getClassObjectMap();
classObjectMap.forEach(item -> {
int i = item.lastIndexOf(".");
String substring = item.substring(i+1);
System.out.println("substring = " + substring);
LiteFlowNodeBuilder.createCommonNode().setId(substring).setName(substring).setClazz(item).build();
});
map.put("interfaceMapRef", classObjectMap);
return Result.buildSuccess(map);
} else if (liteflow.equals(path)) {
try {
try {
String postData = this.getPostData(request);
log.info("HandServlet -> requestHandler start, postData={}", postData);
JSONObject jsonObject = JSONObject.parseObject(postData);
JSONArray checkList = jsonObject.getJSONArray("checkList");
String chainId = (String) jsonObject.get("chainId");
log.info("HandServlet -> requestHandler start, path={},checkList={}", path, checkList);
ArrayList arrayList = new ArrayList();
checkList.forEach(item -> {
String itemStr = (String) item;
int i = itemStr.lastIndexOf(".");
String substring = itemStr.substring(i+1);
arrayList.add(substring);
});
String str = StringUtils.join(arrayList, ",");
log.info("HandServlet -> requestHandler start, str={}", str);
// String elss = "THEN(" + str + ")";
// log.info("HandServlet -> requestHandler start, elss={}", elss);
Condition condition = LiteFlowConditionBuilder.createCondition(ConditionTypeEnum.TYPE_THEN).setValue(str).build();
log.info("HandServlet -> requestHandler start, condition={}", condition);
LiteFlowChainBuilder.createChain().setChainName(chainId).setCondition(condition).build();
} catch (Throwable var3) {
log.error("HandServlet -> requestHandler exception 未知異常, var3={}", var3);
}
} catch (Throwable var3) {
log.info("MqUtil->haveProducer,error", var3);
}
return Result.buildSuccess(false);
} else {
return null;
}
}
public String getPostData(HttpServletRequest request) {
StringBuilder data = new StringBuilder();
String line;
BufferedReader reader;
try {
reader = request.getReader();
while (null != (line = reader.readLine())) {
data.append(line);
}
} catch (IOException e) {
return null;
}
return data.toString();
}
private List<String> getClassObjectMap() {
List<String> result = new ArrayList<>();
WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
Map<String, ServiceScanner> serviceScannerMap = webApplicationContext.getBeansOfType(ServiceScanner.class);
ServiceScanner serviceScanner = serviceScannerMap.get("serviceScanner");
Map<Class<?>, Object> interfaceMapRef = serviceScanner.interfaceMapRef();
if (null != interfaceMapRef) {
//排序 所有接口
List<Map.Entry<Class<?>, Object>> arrayList = new ArrayList<Map.Entry<Class<?>, Object>>(interfaceMapRef.entrySet());
Collections.sort(arrayList, new Comparator<Map.Entry<Class<?>, Object>>() {
@Override
public int compare(Map.Entry<Class<?>, Object> o1, Map.Entry<Class<?>, Object> o2) {
return o1.getKey().getSimpleName().compareTo(o2.getKey().getSimpleName());
}
});
//遍歷 所有接口
for (Map.Entry<Class<?>, Object> entry : arrayList) {
String className = entry.getValue().getClass().getName();
System.out.println("class = " + className);
result.add(className);
// List<Method> interfaceMethodList = Arrays.asList(entry.getKey().getDeclaredMethods());
// //方法列表排序
// Collections.sort(interfaceMethodList, new Comparator<Method>() {
// @Override
// public int compare(Method o1, Method o2) {
// return o1.getName().compareTo(o2.getName());
// }
// });
// for (Method method : interfaceMethodList) {
// System.out.println("method = " + method);
// System.out.println("methodName = " + method.getName());
// System.out.println("methodParameterTypes = " + method.getParameterTypes());
// System.out.println("methodReturn = " + method.getReturnType());
// }
}
}
System.out.println("result = " + result);
return result;
}
private boolean needLogin(HttpServletRequest request, String path) {
return this.isRequireAuth() && !this.alreadyLogin(request) && !this.checkLoginParam(request) && !"/login.html".equals(path) && !path.startsWith("/css") && !path.startsWith("/js") && !path.startsWith("/img");
}
private boolean checkLoginParam(HttpServletRequest request) {
String usernameParam = request.getParameter("loginUsername");
String passwordParam = request.getParameter("loginPassword");
if (null != this.username && null != this.password) {
return this.username.equals(usernameParam) && this.password.equals(passwordParam);
} else {
return false;
}
}
private boolean isRequireAuth() {
return this.username != null;
}
private boolean alreadyLogin(HttpServletRequest request) {
HttpSession session = request.getSession(false);
return session != null && session.getAttribute("lite-flow") != null;
}
private void returnResourceFile(String fileName, String uri, HttpServletResponse response) throws IOException {
String filePath = this.getFilePath(fileName);
if (filePath.endsWith(".html")) {
response.setContentType("text/html; charset=utf-8");
}
if (fileName.endsWith(".jpg")) {
byte[] bytes = Utils.readByteArrayFromResource(filePath);
if (bytes != null) {
response.getOutputStream().write(bytes);
}
} else {
String text = Utils.readFromResource(filePath);
if (text == null) {
response.sendRedirect(uri + "/login.html");
} else {
if (fileName.endsWith(".css")) {
response.setContentType("text/css;charset=utf-8");
} else if (fileName.endsWith(".js")) {
response.setContentType("text/javascript;charset=utf-8");
}
response.getWriter().write(text);
}
}
}
private String getFilePath(String fileName) {
return "view" + fileName;
}
2.2.2配置web.xml
在liteflow-util內web.xml配置自定義的servlet文章來源地址http://www.zghlxwxcb.cn/news/detail-829221.html
<servlet>
<servlet-name>handOfLite</servlet-name>
<servlet-class>com.xx.utils.liteflow.handler.HandServlet</servlet-class>
<init-param>
<param-name>loginUsername</param-name>
<param-value>Username</param-value>
</init-param>
<init-param>
<param-name>loginPassword</param-name>
<param-value>Password</param-value>
</init-param>
<load-on-startup>5</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>handOfLite</servlet-name>
<url-pattern>/hand-of-lite/*</url-pattern>
</servlet-mapping>
2.2.3頁面準備
到了這里,關于Unity UGUI的Physi會隨機改變csRaycaster (物理射線檢測)組件的介紹及使用的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!