.katex { display: block; text-align: center; white-space: nowrap; }
.katex-display > .katex > .katex-html { display: block; }
.katex-display > .katex > .katex-html > .tag { position: absolute; right: 0px; }
.katex { font: 1.21em/1.2 KaTeX_Main, "Times New Roman", serif; text-indent: 0px; text-rendering: auto; }
.katex * { }
.katex .katex-mathml { position: absolute; clip: rect(1px, 1px, 1px, 1px); padding: 0px; border: 0px; height: 1px; width: 1px; overflow: hidden; }
.katex .katex-html { }
.katex .katex-html > .newline { display: block; }
.katex .base { position: relative; display: inline-block; white-space: nowrap; width: min-content; }
.katex .strut { display: inline-block; }
.katex .textbf { font-weight: bold; }
.katex .textit { font-style: italic; }
.katex .textrm { font-family: KaTeX_Main; }
.katex .textsf { font-family: KaTeX_SansSerif; }
.katex .texttt { font-family: KaTeX_Typewriter; }
.katex .mathit { font-family: KaTeX_Math; font-style: italic; }
.katex .mathrm { font-style: normal; }
.katex .mathbf { font-family: KaTeX_Main; font-weight: bold; }
.katex .boldsymbol { font-family: KaTeX_Math; font-weight: bold; font-style: italic; }
.katex .amsrm { font-family: KaTeX_AMS; }
.katex .mathbb, .katex .textbb { font-family: KaTeX_AMS; }
.katex .mathcal { font-family: KaTeX_Caligraphic; }
.katex .mathfrak, .katex .textfrak { font-family: KaTeX_Fraktur; }
.katex .mathtt { font-family: KaTeX_Typewriter; }
.katex .mathscr, .katex .textscr { font-family: KaTeX_Script; }
.katex .mathsf, .katex .textsf { font-family: KaTeX_SansSerif; }
.katex .mainit { font-family: KaTeX_Main; font-style: italic; }
.katex .mainrm { font-family: KaTeX_Main; font-style: normal; }
.katex .vlist-t { display: inline-table; table-layout: fixed; }
.katex .vlist-r { display: table-row; }
.katex .vlist { display: table-cell; vertical-align: bottom; position: relative; }
.katex .vlist > span { display: block; height: 0px; position: relative; }
.katex .vlist > span > span { display: inline-block; }
.katex .vlist > span > .pstrut { overflow: hidden; width: 0px; }
.katex .vlist-t2 { margin-right: -2px; }
.katex .vlist-s { display: table-cell; vertical-align: bottom; font-size: 1px; width: 2px; min-width: 2px; }
.katex .msupsub { text-align: left; }
.katex .mfrac > span > span { text-align: center; }
.katex .mfrac .frac-line { display: inline-block; width: 100%; border-bottom-style: solid; }
.katex .mspace { display: inline-block; }
.katex .llap, .katex .rlap, .katex .clap { width: 0px; position: relative; }
.katex .llap > .inner, .katex .rlap > .inner, .katex .clap > .inner { position: absolute; }
.katex .llap > .fix, .katex .rlap > .fix, .katex .clap > .fix { display: inline-block; }
.katex .llap > .inner { right: 0px; }
.katex .rlap > .inner, .katex .clap > .inner { left: 0px; }
.katex .clap > .inner > span { margin-left: -50%; margin-right: 50%; }
.katex .rule { display: inline-block; border: 0px solid; position: relative; }
.katex .overline .overline-line, .katex .underline .underline-line, .katex .hline { display: inline-block; width: 100%; border-bottom-style: solid; }
.katex .hdashline { display: inline-block; width: 100%; border-bottom-style: dashed; }
.katex .sqrt > .root { margin-left: 0.277778em; margin-right: -0.555556em; }
.katex .sizing, .katex .fontsize-ensurer { display: inline-block; }
.katex .sizing.reset-size1.size1, .katex .fontsize-ensurer.reset-size1.size1 { font-size: 1em; }
.katex .sizing.reset-size1.size2, .katex .fontsize-ensurer.reset-size1.size2 { font-size: 1.2em; }
.katex .sizing.reset-size1.size3, .katex .fontsize-ensurer.reset-size1.size3 { font-size: 1.4em; }
.katex .sizing.reset-size1.size4, .katex .fontsize-ensurer.reset-size1.size4 { font-size: 1.6em; }
.katex .sizing.reset-size1.size5, .katex .fontsize-ensurer.reset-size1.size5 { font-size: 1.8em; }
.katex .sizing.reset-size1.size6, .katex .fontsize-ensurer.reset-size1.size6 { font-size: 2em; }
.katex .sizing.reset-size1.size7, .katex .fontsize-ensurer.reset-size1.size7 { font-size: 2.4em; }
.katex .sizing.reset-size1.size8, .katex .fontsize-ensurer.reset-size1.size8 { font-size: 2.88em; }
.katex .sizing.reset-size1.size9, .katex .fontsize-ensurer.reset-size1.size9 { font-size: 3.456em; }
.katex .sizing.reset-size1.size10, .katex .fontsize-ensurer.reset-size1.size10 { font-size: 4.148em; }
.katex .sizing.reset-size1.size11, .katex .fontsize-ensurer.reset-size1.size11 { font-size: 4.976em; }
.katex .sizing.reset-size2.size1, .katex .fontsize-ensurer.reset-size2.size1 { font-size: 0.833333em; }
.katex .sizing.reset-size2.size2, .katex .fontsize-ensurer.reset-size2.size2 { font-size: 1em; }
.katex .sizing.reset-size2.size3, .katex .fontsize-ensurer.reset-size2.size3 { font-size: 1.16667em; }
.katex .sizing.reset-size2.size4, .katex .fontsize-ensurer.reset-size2.size4 { font-size: 1.33333em; }
.katex .sizing.reset-size2.size5, .katex .fontsize-ensurer.reset-size2.size5 { font-size: 1.5em; }
.katex .sizing.reset-size2.size6, .katex .fontsize-ensurer.reset-size2.size6 { font-size: 1.66667em; }
.katex .sizing.reset-size2.size7, .katex .fontsize-ensurer.reset-size2.size7 { font-size: 2em; }
.katex .sizing.reset-size2.size8, .katex .fontsize-ensurer.reset-size2.size8 { font-size: 2.4em; }
.katex .sizing.reset-size2.size9, .katex .fontsize-ensurer.reset-size2.size9 { font-size: 2.88em; }
.katex .sizing.reset-size2.size10, .katex .fontsize-ensurer.reset-size2.size10 { font-size: 3.45667em; }
.katex .sizing.reset-size2.size11, .katex .fontsize-ensurer.reset-size2.size11 { font-size: 4.14667em; }
.katex .sizing.reset-size3.size1, .katex .fontsize-ensurer.reset-size3.size1 { font-size: 0.714286em; }
.katex .sizing.reset-size3.size2, .katex .fontsize-ensurer.reset-size3.size2 { font-size: 0.857143em; }
.katex .sizing.reset-size3.size3, .katex .fontsize-ensurer.reset-size3.size3 { font-size: 1em; }
.katex .sizing.reset-size3.size4, .katex .fontsize-ensurer.reset-size3.size4 { font-size: 1.14286em; }
.katex .sizing.reset-size3.size5, .katex .fontsize-ensurer.reset-size3.size5 { font-size: 1.28571em; }
.katex .sizing.reset-size3.size6, .katex .fontsize-ensurer.reset-size3.size6 { font-size: 1.42857em; }
.katex .sizing.reset-size3.size7, .katex .fontsize-ensurer.reset-size3.size7 { font-size: 1.71429em; }
.katex .sizing.reset-size3.size8, .katex .fontsize-ensurer.reset-size3.size8 { font-size: 2.05714em; }
.katex .sizing.reset-size3.size9, .katex .fontsize-ensurer.reset-size3.size9 { font-size: 2.46857em; }
.katex .sizing.reset-size3.size10, .katex .fontsize-ensurer.reset-size3.size10 { font-size: 2.96286em; }
.katex .sizing.reset-size3.size11, .katex .fontsize-ensurer.reset-size3.size11 { font-size: 3.55429em; }
.katex .sizing.reset-size4.size1, .katex .fontsize-ensurer.reset-size4.size1 { font-size: 0.625em; }
.katex .sizing.reset-size4.size2, .katex .fontsize-ensurer.reset-size4.size2 { font-size: 0.75em; }
.katex .sizing.reset-size4.size3, .katex .fontsize-ensurer.reset-size4.size3 { font-size: 0.875em; }
.katex .sizing.reset-size4.size4, .katex .fontsize-ensurer.reset-size4.size4 { font-size: 1em; }
.katex .sizing.reset-size4.size5, .katex .fontsize-ensurer.reset-size4.size5 { font-size: 1.125em; }
.katex .sizing.reset-size4.size6, .katex .fontsize-ensurer.reset-size4.size6 { font-size: 1.25em; }
.katex .sizing.reset-size4.size7, .katex .fontsize-ensurer.reset-size4.size7 { font-size: 1.5em; }
.katex .sizing.reset-size4.size8, .katex .fontsize-ensurer.reset-size4.size8 { font-size: 1.8em; }
.katex .sizing.reset-size4.size9, .katex .fontsize-ensurer.reset-size4.size9 { font-size: 2.16em; }
.katex .sizing.reset-size4.size10, .katex .fontsize-ensurer.reset-size4.size10 { font-size: 2.5925em; }
.katex .sizing.reset-size4.size11, .katex .fontsize-ensurer.reset-size4.size11 { font-size: 3.11em; }
.katex .sizing.reset-size5.size1, .katex .fontsize-ensurer.reset-size5.size1 { font-size: 0.555556em; }
.katex .sizing.reset-size5.size2, .katex .fontsize-ensurer.reset-size5.size2 { font-size: 0.666667em; }
.katex .sizing.reset-size5.size3, .katex .fontsize-ensurer.reset-size5.size3 { font-size: 0.777778em; }
.katex .sizing.reset-size5.size4, .katex .fontsize-ensurer.reset-size5.size4 { font-size: 0.888889em; }
.katex .sizing.reset-size5.size5, .katex .fontsize-ensurer.reset-size5.size5 { font-size: 1em; }
.katex .sizing.reset-size5.size6, .katex .fontsize-ensurer.reset-size5.size6 { font-size: 1.11111em; }
.katex .sizing.reset-size5.size7, .katex .fontsize-ensurer.reset-size5.size7 { font-size: 1.33333em; }
.katex .sizing.reset-size5.size8, .katex .fontsize-ensurer.reset-size5.size8 { font-size: 1.6em; }
.katex .sizing.reset-size5.size9, .katex .fontsize-ensurer.reset-size5.size9 { font-size: 1.92em; }
.katex .sizing.reset-size5.size10, .katex .fontsize-ensurer.reset-size5.size10 { font-size: 2.30444em; }
.katex .sizing.reset-size5.size11, .katex .fontsize-ensurer.reset-size5.size11 { font-size: 2.76444em; }
.katex .sizing.reset-size6.size1, .katex .fontsize-ensurer.reset-size6.size1 { font-size: 0.5em; }
.katex .sizing.reset-size6.size2, .katex .fontsize-ensurer.reset-size6.size2 { font-size: 0.6em; }
.katex .sizing.reset-size6.size3, .katex .fontsize-ensurer.reset-size6.size3 { font-size: 0.7em; }
.katex .sizing.reset-size6.size4, .katex .fontsize-ensurer.reset-size6.size4 { font-size: 0.8em; }
.katex .sizing.reset-size6.size5, .katex .fontsize-ensurer.reset-size6.size5 { font-size: 0.9em; }
.katex .sizing.reset-size6.size6, .katex .fontsize-ensurer.reset-size6.size6 { font-size: 1em; }
.katex .sizing.reset-size6.size7, .katex .fontsize-ensurer.reset-size6.size7 { font-size: 1.2em; }
.katex .sizing.reset-size6.size8, .katex .fontsize-ensurer.reset-size6.size8 { font-size: 1.44em; }
.katex .sizing.reset-size6.size9, .katex .fontsize-ensurer.reset-size6.size9 { font-size: 1.728em; }
.katex .sizing.reset-size6.size10, .katex .fontsize-ensurer.reset-size6.size10 { font-size: 2.074em; }
.katex .sizing.reset-size6.size11, .katex .fontsize-ensurer.reset-size6.size11 { font-size: 2.488em; }
.katex .sizing.reset-size7.size1, .katex .fontsize-ensurer.reset-size7.size1 { font-size: 0.416667em; }
.katex .sizing.reset-size7.size2, .katex .fontsize-ensurer.reset-size7.size2 { font-size: 0.5em; }
.katex .sizing.reset-size7.size3, .katex .fontsize-ensurer.reset-size7.size3 { font-size: 0.583333em; }
.katex .sizing.reset-size7.size4, .katex .fontsize-ensurer.reset-size7.size4 { font-size: 0.666667em; }
.katex .sizing.reset-size7.size5, .katex .fontsize-ensurer.reset-size7.size5 { font-size: 0.75em; }
.katex .sizing.reset-size7.size6, .katex .fontsize-ensurer.reset-size7.size6 { font-size: 0.833333em; }
.katex .sizing.reset-size7.size7, .katex .fontsize-ensurer.reset-size7.size7 { font-size: 1em; }
.katex .sizing.reset-size7.size8, .katex .fontsize-ensurer.reset-size7.size8 { font-size: 1.2em; }
.katex .sizing.reset-size7.size9, .katex .fontsize-ensurer.reset-size7.size9 { font-size: 1.44em; }
.katex .sizing.reset-size7.size10, .katex .fontsize-ensurer.reset-size7.size10 { font-size: 1.72833em; }
.katex .sizing.reset-size7.size11, .katex .fontsize-ensurer.reset-size7.size11 { font-size: 2.07333em; }
.katex .sizing.reset-size8.size1, .katex .fontsize-ensurer.reset-size8.size1 { font-size: 0.347222em; }
.katex .sizing.reset-size8.size2, .katex .fontsize-ensurer.reset-size8.size2 { font-size: 0.416667em; }
.katex .sizing.reset-size8.size3, .katex .fontsize-ensurer.reset-size8.size3 { font-size: 0.486111em; }
.katex .sizing.reset-size8.size4, .katex .fontsize-ensurer.reset-size8.size4 { font-size: 0.555556em; }
.katex .sizing.reset-size8.size5, .katex .fontsize-ensurer.reset-size8.size5 { font-size: 0.625em; }
.katex .sizing.reset-size8.size6, .katex .fontsize-ensurer.reset-size8.size6 { font-size: 0.694444em; }
.katex .sizing.reset-size8.size7, .katex .fontsize-ensurer.reset-size8.size7 { font-size: 0.833333em; }
.katex .sizing.reset-size8.size8, .katex .fontsize-ensurer.reset-size8.size8 { font-size: 1em; }
.katex .sizing.reset-size8.size9, .katex .fontsize-ensurer.reset-size8.size9 { font-size: 1.2em; }
.katex .sizing.reset-size8.size10, .katex .fontsize-ensurer.reset-size8.size10 { font-size: 1.44028em; }
.katex .sizing.reset-size8.size11, .katex .fontsize-ensurer.reset-size8.size11 { font-size: 1.72778em; }
.katex .sizing.reset-size9.size1, .katex .fontsize-ensurer.reset-size9.size1 { font-size: 0.289352em; }
.katex .sizing.reset-size9.size2, .katex .fontsize-ensurer.reset-size9.size2 { font-size: 0.347222em; }
.katex .sizing.reset-size9.size3, .katex .fontsize-ensurer.reset-size9.size3 { font-size: 0.405093em; }
.katex .sizing.reset-size9.size4, .katex .fontsize-ensurer.reset-size9.size4 { font-size: 0.462963em; }
.katex .sizing.reset-size9.size5, .katex .fontsize-ensurer.reset-size9.size5 { font-size: 0.520833em; }
.katex .sizing.reset-size9.size6, .katex .fontsize-ensurer.reset-size9.size6 { font-size: 0.578704em; }
.katex .sizing.reset-size9.size7, .katex .fontsize-ensurer.reset-size9.size7 { font-size: 0.694444em; }
.katex .sizing.reset-size9.size8, .katex .fontsize-ensurer.reset-size9.size8 { font-size: 0.833333em; }
.katex .sizing.reset-size9.size9, .katex .fontsize-ensurer.reset-size9.size9 { font-size: 1em; }
.katex .sizing.reset-size9.size10, .katex .fontsize-ensurer.reset-size9.size10 { font-size: 1.20023em; }
.katex .sizing.reset-size9.size11, .katex .fontsize-ensurer.reset-size9.size11 { font-size: 1.43981em; }
.katex .sizing.reset-size10.size1, .katex .fontsize-ensurer.reset-size10.size1 { font-size: 0.24108em; }
.katex .sizing.reset-size10.size2, .katex .fontsize-ensurer.reset-size10.size2 { font-size: 0.289296em; }
.katex .sizing.reset-size10.size3, .katex .fontsize-ensurer.reset-size10.size3 { font-size: 0.337512em; }
.katex .sizing.reset-size10.size4, .katex .fontsize-ensurer.reset-size10.size4 { font-size: 0.385728em; }
.katex .sizing.reset-size10.size5, .katex .fontsize-ensurer.reset-size10.size5 { font-size: 0.433944em; }
.katex .sizing.reset-size10.size6, .katex .fontsize-ensurer.reset-size10.size6 { font-size: 0.48216em; }
.katex .sizing.reset-size10.size7, .katex .fontsize-ensurer.reset-size10.size7 { font-size: 0.578592em; }
.katex .sizing.reset-size10.size8, .katex .fontsize-ensurer.reset-size10.size8 { font-size: 0.694311em; }
.katex .sizing.reset-size10.size9, .katex .fontsize-ensurer.reset-size10.size9 { font-size: 0.833173em; }
.katex .sizing.reset-size10.size10, .katex .fontsize-ensurer.reset-size10.size10 { font-size: 1em; }
.katex .sizing.reset-size10.size11, .katex .fontsize-ensurer.reset-size10.size11 { font-size: 1.19961em; }
.katex .sizing.reset-size11.size1, .katex .fontsize-ensurer.reset-size11.size1 { font-size: 0.200965em; }
.katex .sizing.reset-size11.size2, .katex .fontsize-ensurer.reset-size11.size2 { font-size: 0.241158em; }
.katex .sizing.reset-size11.size3, .katex .fontsize-ensurer.reset-size11.size3 { font-size: 0.28135em; }
.katex .sizing.reset-size11.size4, .katex .fontsize-ensurer.reset-size11.size4 { font-size: 0.321543em; }
.katex .sizing.reset-size11.size5, .katex .fontsize-ensurer.reset-size11.size5 { font-size: 0.361736em; }
.katex .sizing.reset-size11.size6, .katex .fontsize-ensurer.reset-size11.size6 { font-size: 0.401929em; }
.katex .sizing.reset-size11.size7, .katex .fontsize-ensurer.reset-size11.size7 { font-size: 0.482315em; }
.katex .sizing.reset-size11.size8, .katex .fontsize-ensurer.reset-size11.size8 { font-size: 0.578778em; }
.katex .sizing.reset-size11.size9, .katex .fontsize-ensurer.reset-size11.size9 { font-size: 0.694534em; }
.katex .sizing.reset-size11.size10, .katex .fontsize-ensurer.reset-size11.size10 { font-size: 0.833601em; }
.katex .sizing.reset-size11.size11, .katex .fontsize-ensurer.reset-size11.size11 { font-size: 1em; }
.katex .delimsizing.size1 { font-family: KaTeX_Size1; }
.katex .delimsizing.size2 { font-family: KaTeX_Size2; }
.katex .delimsizing.size3 { font-family: KaTeX_Size3; }
.katex .delimsizing.size4 { font-family: KaTeX_Size4; }
.katex .delimsizing.mult .delim-size1 > span { font-family: KaTeX_Size1; }
.katex .delimsizing.mult .delim-size4 > span { font-family: KaTeX_Size4; }
.katex .nulldelimiter { display: inline-block; width: 0.12em; }
.katex .delimcenter { position: relative; }
.katex .op-symbol { position: relative; }
.katex .op-symbol.small-op { font-family: KaTeX_Size1; }
.katex .op-symbol.large-op { font-family: KaTeX_Size2; }
.katex .op-limits > .vlist-t { text-align: center; }
.katex .accent > .vlist-t { text-align: center; }
.katex .accent .accent-body:not(.accent-full) { width: 0px; }
.katex .accent .accent-body { position: relative; }
.katex .overlay { display: block; }
.katex .mtable .vertical-separator { display: inline-block; margin: 0px -0.025em; border-right: 0.05em solid; }
.katex .mtable .vs-dashed { border-right: 0.05em dashed; }
.katex .mtable .arraycolsep { display: inline-block; }
.katex .mtable .col-align-c > .vlist-t { text-align: center; }
.katex .mtable .col-align-l > .vlist-t { text-align: left; }
.katex .mtable .col-align-r > .vlist-t { text-align: right; }
.katex .svg-align { text-align: left; }
.katex svg, .screenShotTempCanvas { display: block; position: absolute; width: 100%; height: inherit; fill: currentcolor; stroke: currentcolor; fill-rule: nonzero; fill-opacity: 1; stroke-width: 1; stroke-linecap: butt; stroke-linejoin: miter; stroke-miterlimit: 4; stroke-dasharray: none; stroke-dashoffset: 0; stroke-opacity: 1; }
.katex svg path { stroke: none; }
.katex .stretchy { width: 100%; display: block; position: relative; overflow: hidden; }
.katex .stretchy::before, .katex .stretchy::after { content: ""; }
.katex .hide-tail { width: 100%; position: relative; overflow: hidden; }
.katex .halfarrow-left { position: absolute; left: 0px; width: 50.2%; overflow: hidden; }
.katex .halfarrow-right { position: absolute; right: 0px; width: 50.2%; overflow: hidden; }
.katex .brace-left { position: absolute; left: 0px; width: 25.1%; overflow: hidden; }
.katex .brace-center { position: absolute; left: 25%; width: 50%; overflow: hidden; }
.katex .brace-right { position: absolute; right: 0px; width: 25.1%; overflow: hidden; }
.katex .x-arrow-pad { padding: 0px 0.5em; }
.katex .x-arrow, .katex .mover, .katex .munder { text-align: center; }
.katex .boxpad { padding: 0px 0.3em; }
.katex .fbox { box-sizing: border-box; border: 0.04em solid black; }
.katex .fcolorbox { box-sizing: border-box; border: 0.04em solid; }
.katex .cancel-pad { padding: 0px 0.2em; }
.katex .cancel-lap { margin-left: -0.2em; margin-right: -0.2em; }
.katex .sout { border-bottom-style: solid; border-bottom-width: 0.08em; }
.output_wrapper pre code { display: -webkit-box !important; }
.output_wrapper .hljs{color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;}
.output_wrapper .hljs-params{color: rgb(255, 152, 35);}
.output_wrapper .hljs-number,.output_wrapper .hljs-literal,.output_wrapper .hljs-symbol,.output_wrapper .hljs-bullet{color: rgb(174, 135, 250);}
.output_wrapper .hljs-function,.output_wrapper .hljs-built_in,.output_wrapper .hljs-name,.output_wrapper .hljs-keyword,.output_wrapper .hljs-selector-tag,.output_wrapper .hljs-deletion{color: rgb(248, 35, 117);}
.output_wrapper .hljs-variable,.output_wrapper .hljs-template-variable,.output_wrapper .hljs-link{color: rgb(98, 151, 85);}
.output_wrapper .hljs-comment,.output_wrapper .hljs-quote{color: rgb(128, 128, 128);}
.output_wrapper .hljs-meta{color: rgb(91, 218, 237);}
.output_wrapper .hljs-string,.output_wrapper .hljs-attribute,.output_wrapper .hljs-addition{color: rgb(238, 220, 112);}
.output_wrapper .hljs-attr,.output_wrapper .hljs-section,.output_wrapper .hljs-title,.output_wrapper .hljs-type{color: rgb(165, 218, 45);}
.output_wrapper .hljs-selector-class{color: rgb(165, 218, 45);}
.output_wrapper .hljs-emphasis{font-style: italic;}
.output_wrapper .hljs-strong{font-weight: bold;}
.output_wrapper pre code {line-height: 18px; font-size: 14px; font-weight: normal; word-spacing: 0px; letter-spacing: 0px;}
.output_wrapper{font-size: 16px; color: rgb(62, 62, 62); line-height: 1.6; word-spacing: 0px; letter-spacing: 0px; font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif; background-image: linear-gradient(90deg, rgba(50, 0, 0, 0.05) 3%, rgba(0, 0, 0, 0) 3%), linear-gradient(360deg, rgba(50, 0, 0, 0.05) 3%, rgba(0, 0, 0, 0) 3%); background-size: 20px 20px; background-position: center center;}
.output_wrapper *{font-size: inherit; color: inherit; line-height: inherit; margin: 0px; padding: 0px;}
.output_wrapper p{margin: 1.5em 0px;}
.output_wrapper h1,.output_wrapper h2,.output_wrapper h3,.output_wrapper h4,.output_wrapper h5,.output_wrapper h6{margin: 1.5em 0px; font-weight: bold;}
.output_wrapper h1{font-size: 1.6em;}
.output_wrapper h2{font-size: 1.4em;}
.output_wrapper h3{font-size: 1.3em;}
.output_wrapper h4{font-size: 1.2em;}
.output_wrapper h5{font-size: 1em;}
.output_wrapper h6{font-size: 1em;}
.output_wrapper ul,.output_wrapper ol{padding-left: 32px;}
.output_wrapper ul{list-style-type: disc;}
.output_wrapper ol{list-style-type: decimal;}
.output_wrapper li *{}
.output_wrapper li{margin-bottom: 0.5em;}
.output_wrapper .code_size_default{line-height: 18px; font-size: 14px; font-weight: normal; word-spacing: 0px; letter-spacing: 0px;}
.output_wrapper .code_size_tight{line-height: 15px; font-size: 11px; font-weight: normal; word-spacing: -3px; letter-spacing: 0px;}
.output_wrapper pre code{font-family: Consolas, Inconsolata, Courier, monospace; border-radius: 0px;}
.output_wrapper blockquote{display: block; padding: 15px 15px 15px 1rem; font-size: 0.9em; margin: 1em 0px; color: rgb(129, 145, 152); border-left: 6px solid rgb(220, 230, 240); background: rgb(242, 247, 251); overflow: auto; overflow-wrap: normal; word-break: normal;}
.output_wrapper blockquote p{margin: 0px;}
.output_wrapper a{text-decoration: none; color: rgb(30, 107, 184); overflow-wrap: break-word;}
.output_wrapper strong{font-weight: bold;}
.output_wrapper em{font-style: italic;}
.output_wrapper del{font-style: italic;}
.output_wrapper strong em{font-weight: bold;}
.output_wrapper hr{height: 1px; margin: 1.5rem 0px; border-right: none; border-bottom: none; border-left: none; border-image: initial; border-top: 1px dashed rgb(165, 165, 165);}
.output_wrapper code{overflow-wrap: break-word; padding: 2px 4px; border-radius: 4px; margin: 0px 2px; color: rgb(233, 105, 0); background: rgb(248, 248, 248);}
.output_wrapper img{display: block; margin: 0px auto; max-width: 100%;}
.output_wrapper figcaption{margin-top: 10px; text-align: center; color: rgb(153, 153, 153); font-size: 0.7em;}
.output_wrapper table{display: table; width: 100%; text-align: left;}
.output_wrapper tbody{border: 0px;}
.output_wrapper table tr{border-width: 1px 0px 0px; border-right-style: initial; border-bottom-style: initial; border-left-style: initial; border-right-color: initial; border-bottom-color: initial; border-left-color: initial; border-image: initial; border-top-style: solid; border-top-color: rgb(204, 204, 204); background-color: white;}
.output_wrapper table tr th,.output_wrapper table tr td{font-size: 1em; border: 1px solid rgb(204, 204, 204); padding: 0.5em 1em; text-align: left;}
.output_wrapper table tr th{font-weight: bold; background-color: rgb(240, 240, 240);}
.output_wrapper .katex-display{font-size: 1.22em;}
.output_wrapper .katex{padding: 8px 3px;}
.output_wrapper .katex-display > .katex{display: inline-block; text-align: center; padding: 3px;}
.output_wrapper .katex img{display: inline-block; vertical-align: middle;}
.output_wrapper a[href^="#"] sup{vertical-align: super; margin: 0px 2px; padding: 1px 3px; color: rgb(255, 255, 255); background: rgb(102, 102, 102); font-size: 0.7em;}
.output_wrapper .task-list-list{list-style-type: none;}
.output_wrapper .task-list-list.checked{color: rgb(62, 62, 62);}
.output_wrapper .task-list-list.uncheck{color: rgb(191, 193, 191);}
.output_wrapper .task-list-list .icon_uncheck,.output_wrapper .task-list-list .icon_check{display: inline-block; vertical-align: middle; margin-right: 10px;}
.output_wrapper .task-list-list .icon_check::before{content: "√"; border: 2px solid rgb(62, 62, 62); color: red;}
.output_wrapper .task-list-list .icon_uncheck::before{content: "x"; border: 2px solid rgb(191, 193, 191); color: rgb(191, 193, 191);}
.output_wrapper .task-list-list .icon_check::before,.output_wrapper .task-list-list .icon_uncheck::before{padding: 2px 8px 2px 5px; border-radius: 5px;}
.output_wrapper .toc{margin-left: 25px;}
.output_wrapper .toc_item{display: block;}
.output_wrapper .toc_left{margin-left: 25px;}
.output_wrapper pre code{border-radius: 3px; border-width: 1px 1px 1px 6px; border-style: solid; border-color: rgb(204, 204, 204) rgb(204, 204, 204) rgb(204, 204, 204) rgb(33, 152, 99);}
.output_wrapper pre code .linenum{padding-right: 20px; word-spacing: 0px;}
.output_wrapper .hljs{color: rgb(169, 183, 198); background: rgb(40, 43, 46); display: block; overflow-x: auto; padding: 0.5em;}
.output_wrapper .hljs-params{color: rgb(255, 152, 35);}
.output_wrapper .hljs-number,.output_wrapper .hljs-literal,.output_wrapper .hljs-symbol,.output_wrapper .hljs-bullet{color: rgb(174, 135, 250);}
.output_wrapper .hljs-function,.output_wrapper .hljs-built_in,.output_wrapper .hljs-name,.output_wrapper .hljs-keyword,.output_wrapper .hljs-selector-tag,.output_wrapper .hljs-deletion{color: rgb(248, 35, 117);}
.output_wrapper .hljs-variable,.output_wrapper .hljs-template-variable,.output_wrapper .hljs-link{color: rgb(98, 151, 85);}
.output_wrapper .hljs-comment,.output_wrapper .hljs-quote{color: rgb(128, 128, 128);}
.output_wrapper .hljs-meta{color: rgb(91, 218, 237);}
.output_wrapper .hljs-string,.output_wrapper .hljs-attribute,.output_wrapper .hljs-addition{color: rgb(238, 220, 112);}
.output_wrapper .hljs-attr,.output_wrapper .hljs-section,.output_wrapper .hljs-title,.output_wrapper .hljs-type{color: rgb(165, 218, 45);}
.output_wrapper .hljs-selector-class{color: rgb(165, 218, 45);}
.output_wrapper .hljs-emphasis{font-style: italic;}
.output_wrapper .hljs-strong{font-weight: bold;}
.output_wrapper p{margin: 1.5em 0px;}
.output_wrapper h1,.output_wrapper h2,.output_wrapper h3,.output_wrapper h4,.output_wrapper h5,.output_wrapper h6{margin: 1.5em 0px; font-weight: bold;}
.output_wrapper h1{font-size: 1.6em;}
.output_wrapper h2{font-size: 1.4em;}
.output_wrapper h3{font-size: 1.3em;}
.output_wrapper h4{font-size: 1.2em;}
.output_wrapper h5{font-size: 1em;}
.output_wrapper h6{font-size: 1em;}
.output_wrapper h3{border-bottom: 2px solid rgb(62, 62, 62); margin-bottom: 50px;}
.output_wrapper h3 span{display: inline-block; padding: 10px 0px;}
.output_wrapper h3 span::first-letter,.output_wrapper h3 .firstletter{color: rgb(255, 255, 255); padding: 10px 15px; margin-right: 20px; background: rgb(62, 62, 62);}
-->
在本篇教程中,你将学习如何使用行为树和AI感知来创建一个能四处走动,攻击敌人的简单AI。
在视频游戏中,人工智能(AI)通常指的是拥有自主决策行为的非玩家角色。AI可以是看到玩家然后进行攻击的简单角色,也可以是即时策略(RTS)游戏里的强大对手。
在Unreal引擎里,我们可以通过行为树创建AI。行为树是一个决定AI做哪种行为的实时决策系统。比如,如果AI有战斗和逃跑两种行为。你可以创建行为树,让AI在高于50%血量时进行战斗,低于50%血量时逃跑。
在本篇教程中,你将学习到:
- 创建AI实体用于控制角色单位
- 创建并使用行为树和黑板
- 使用AI感知让角色单位获得视野
- 创建行为让角色单位四处走动并攻击敌人
起步入门
下载示例项目并解压。进入项目文件夹,双击MuffinWar.uproject打开项目。
按下Play运行游戏,在围栏内点击左键生成蘑菇小人。
在本例中,我们将创建一个能四处走动的AI,当其他蘑菇小人进入AI的视野时,AI会追逐对方并进行攻击。
要创建一个AI角色,我们需要三个元素:
- 身体:这个是角色的物理表现,在本例中,蘑菇小人就是身体
- 灵魂:这个是控制角色行为的实体,既能是玩家本身,也可以是AI
- 大脑: AI进行决策行为的逻辑,我们可以用C++代码,蓝图或者是行为树来实现逻辑。
现在我们已经有了身体,接着要搞来灵魂和大脑。首先,我们要创建控制器作为灵魂。
什么是控制器?
控制器是一个能控制角色单位的非物理Actor。这里所说的“控制”,具体指的是什么意思呢?
对于玩家而言,控制指的是能通过按键操控角色单位。控制器获取玩家输入,并将输入直接传给角色。当然,控制器也可以获取输入进行处理,然后再告诉角色单位做哪个行为。
对于AI来说,角色单位就是由控制器或“大脑”(取决于实现方式)来通知其做什么行为的。
为了用AI控制蘑菇小人,我们需要创建一类特殊的控制器——AI控制器。
创建AI控制器
打开Characters\Muffin\AI目录并创建Blueprint Class,选中AIController作为父类并命名为AIC_Muffin。
接着,我们需要让蘑菇小人使用这个AI控制器,打开Characters\Muffin\Blueprints并双击打开BP_Muffin。
默认情况下,Details面板会显示蓝图的默认设置,如果没有显示,就点击Toolbar的Class Defaults。
在Details面板找到Pawn设置,将AI Controller Class设为AIC_Muffin,这样当蘑菇小人生成时,就会对应生成一个AI控制器实例。
由于我们要动态生成蘑菇小人,Auto Possess AI要设成Spawned。这样当蘑菇小人生成时,AIC_Muffin就会自动控制BP_Muffin。
点击Compile并关闭BP_Muffin。
现在,我们要来创建决策蘑菇小人行为的逻辑,就要用上行为树。
创建行为树
打开Characters\Muffin\AI目录,并选择Add New\Artificial Intelligence\Behavior Tree,将其命名为BT_Muffin并打开。
行为树编辑器
行为树编辑器包含3个新面板:
- Behavior Tree:这个图表面板用于创建行为树节点
- Details:展示选中节点的参数
- Blackboard:展示黑板的所有键值(后续讲解)和其对应数值。只有在游戏运行时才会有显示
像蓝图一样,行为树也是由节点构成的。行为树有4类节点,前两种分别是任务(tasks)和组合(composites)节点。
什么是任务和组合节点?
顾名思义,任务节点负责完成具体任务,可以是表现一套连招这样的复杂任务,也可以是原地等待这样的简单任务。
要完成多个任务,我们就要用上组合节点。一个行为树由许多分支(行为)组成。每个分支的根节点,都是一个组合节点。不同类型的组合节点,执行其子节点的方式也各不相同。
比如,我们有一组如下序列的行为:
要按顺序执行每个行为,我们就要用上Sequence组合节点,因为Sequence节点能够从左至右的执行子节点,图表看起来是这样的:
如果Sequence的任意节点执行失败,整个Sequence节点就会停止执行。
比如,如果角色无法移动到敌人身边,Move To Enemy节点就执行失败了,这样Rotate Towards Enemy和Attack节点也就无法继续执行了。反之,如果角色成功移动到敌人边上,就能执行随后两个节点。
后续我们还会学习Selector组合节点,不过现在先让我们用Sequence节点实现角色随机移动到某个位置并原地停留。
随机移动位置
首先,创建Sequence节点并与Root节点相连。
接着,我们需要让角色移动起来,创建MoveTo节点与Sequence节点相连,这个节点可以驱动角色移动到特定位置或Actor。
随后,创建Wait节点与Sequence节点相连,确保将其放置在MoveTo节点右边,放置顺序非常重要,因为子节点是按照从左到右的顺序执行的。
恭喜你,你刚刚创建了你的第一个行为!它将会驱动角色移动到指定位置并原地停留数秒。
为了让角色移动,我们还需要指定要移动的位置。由于MoveTo节点只接受由黑板提供的数值,我们要先创建一个黑板。
创建黑板
黑板是一个单纯用来存放变量(键值)的资源。我们可以将其理解为AI的内存。
虽然黑板不是必须使用的,但它确实为我们读取,存取数据提供了极大便利,这么说的原因是很多行为树节点只接受黑板键值作为参数输入。
要创建一个黑板,我们在Content Browser选择新建Add New\Artificial Intelligence\Blackboard,将其命名为BB_Muffin并打开。
黑板编辑器
黑板编辑器由2个面板组成:
- Blackboard:展示所有键值列表
- Blackboard Details:展示所选键值的参数
现在,我们要创建一个键值用于存放目标位置。
创建目标位置键值
由于是3D空间里的一个位置点,我们需要用Vector来进行存储。点击New Key并选择Vector,将其命名为TargetLocation。
接着,我们需要随机生成一个位置并将其存在黑板里,我们就需要用到第三种类型的行为树节点:服务(service)节点。
什么是服务节点?
服务节点类似于任务节点,用于完成一些事情。然而,不同于操控角色做特定行为,服务节点用于执行检查或更新黑板操作。
服务并不是独立节点,而是依附于任务节点或者组合节点。这样使得行为树更加简洁易于组织,不会横生太多节点。如果我们用任务节点来实现,效果如下图所示:
如果用服务节点来实现,则如下图所示:
现在,让我们来创建一个生成随机位置的服务吧。
创建服务
回到BT_Muffin并点击New Service。
这样就会新建一个服务并自动打开,我们回到Content Browser将其重命名为BTService_SetRandomLocation。
服务应当且仅当在角色准备移动时才执行,因此我们要将它附着在MoveTo节点上。
打开BT_Muffin,右键点击MoveTo节点,从弹出菜单选择Add Service\BTService Set Random Location。
现在,当MoveTo激活执行时,BTService_SetRandomLocation也会跟着激活执行。
接着,我们需要随机生成目标点位置。
生成随机位置
打开BTService_SetRandomLocation。
为了监听获知服务何时触发执行,我们创建Event Receive Activation AI节点,这个节点会在服务父类(所附着的节点)激活时触发执行。
为了生成随机位置,添加如下高亮节点,确保将Radius设置为500。
这样就能返回得到该角色500单位半径内的一个随机可达目标点。
接下来,我们需要将位置数据存储到黑板里。有两种方式指定要存放的键值:
- 我们可以使用Make Literal Name节点指定键值名字
- 我们可以将变量暴露给行为树,这样就能在行为树里通过下拉列表选中变量
这里我们使用第二种方法。创建类型为Blackboard Key Selector的变量。将其命名为BlackboardKey并启用Instance Editable,这样行为树里的服务就会出现对应变量。
随后,创建如下高亮节点:
小结:
- Event Receive Activation AI节点会在其父类(本例中的MoveTo节点)激活时执行
- GetRandomPointInNavigableRadius节点返回角色500单位半径内的一个随机可达目标点
- Set Blackboard Value as Vector节点将一个黑板键值(BlackboardKey)数值设为随机位置点
点击Compile并关闭BTService_SetRandomLocation。
接着,我们需要让行为树来使用这个黑板值。
使用黑板
打开BT_Muffin并确保没有选中任何东西。在Details面板的Behavior Tree设置处,将Blackboard Asset设为BB_Muffin。
然后MoveTo和BTService_SetRandomLocation就会自动使用黑板的第一个键值,在本例中,就是TargetLocation。
最后,我们需要让AI控制器来运行行为树。
运行行为树
打开AIC_Muffin并连接Run Behavior Tree节点与Event BeginPlay节点,将BTAsset设为BT_Muffin。
这样当AIC_Controller生成时就会执行BT_Muffin。
点击Compile并返回主编辑器,按下Play运行游戏,生成一些蘑菇小人,观察它们四处走动吧。
虽然设置很繁琐,我们还是搞定了!接着,我们要进一步设置AI控制器,让它可以在一定范围内感知敌人所在。要实现这点,就要使用AI感知(AI Perception)。
设置AI感知
AI感知是一个可以添加给Actor的组件,通过它,我们可以给AI添加感官能力(如视觉和听觉)。
打开AIC_Muffin并添加AIPerception。
接着,我们要添加一个感官,由于我们想要蘑菇小人能够感知到其他小人靠近,我们给它加上视觉感官。
选中AIPerception并在Details面板的AI Perception设置处,给Senses Config添加新元素。
将元素0设置为AI Sight config并展开它。
对于视觉有3个主要设置:
- Sight Radius:蘑菇小人的最远视觉范围,将其设置为3000。
- Lose Sight Radius:如果蘑菇小人已经看到了敌人,那敌人要逃离小人视野的距离,将其设置为3500。
- Peripheral Vision Half Angle Degrees:决定蘑菇小人视野的角度,将其设置为45,蘑菇小人就会有90度的范围视角。
默认情况下,AI感知只检测敌人(被指定为不同队伍(team)的Actor)。然而,Actor默认是没有设置队伍的,如果Actor没有队伍,AI感知就会将其认为中立(neutral)角色。
截至目前,还没有方法能通过蓝图设置Actor的队伍,退而求其次,我们展开Detection by Affiliation设置,启用Detect Neutrals。
点击Compile并回到主编辑器。按下Play运行游戏来生成蘑菇。按下 ‘ 键可以显示AI调试信息,按下小键盘的数字键4可以可视化AI感知组件。当蘑菇小人进入视野时,就会显示绿色球体。
接着,我们要让蘑菇小人往敌人的方向走去。要实现这点,行为树就要了解敌人的信息,我们通过在黑板存储敌人的引用来完成这件事。
创建敌人键值
打开BB_Muffin并添加类型为Object的键值,将其命名为Enemy。
现在,我们还不能在MoveTo节点使用Enemy,因为其键值类型为Object,但MoveTo只接受Vector或Actor类型的键值。
为了解决这点,我们选中Enemy并展开Key Type,将Base Class设置为Actor。这样行为树就能将Enemy识别为Actor了。
关闭BB_Muffin,现在,我们要创建一个行为让AI向敌人走去。
朝敌人移动
打开BT_Muffin并断开Sequence和Root连接。我们可以通过按住Alt键点击连线来做到,并将移动子树移到一边。
接着,创建如下高亮节点,并将Blackboard Key设置为Enemy:
这样角色就会朝Enemy走去。有时候,角色不会刚好面对着它的目标,所以我们还需要用上Rotate to face BB entry节点。
现在,我们需要在AI感知检测到其他蘑菇时,将其设置为Enemy的值。
设置敌人键值
打开AIC_Muffin并选中AIPerception组件,添加Perception Updated事件。
只要感官发生更新,这个事件就会触发执行。在本例中,当AI获得或丢失了某物体的视野,这个事件就会执行,并提供了其当前所能感知到的Actor列表。
添加如下高亮节点,并确保将Make Literal Name节点设置为Enemy。
这样就可以判断AI目前有没有敌人对象,如果没有,我们就要给它设置一个敌人,因此添加如下节点:
小结:
- IsValid节点负责判断Enemy键值是否有值
- 如果还没设置,遍历当前所有检测到的Actor
- Cast To BP_Muffin节点负责检查Actor是否为蘑菇
- 如果是蘑菇,进一步判断是否已死亡
- 如果IsDead返回false,将蘑菇设置为新敌人,并退出循环
点击Compile并关闭AIC_Muffin,按下Play运行游戏并生成两个蘑菇小人,其中一个生成暴露在另一个面前,后者就会自动向前者走过去。
接着,你要创建一个自定义任务,让蘑菇小人可以表演攻击行为。
创建攻击任务
我们可以直接在Content Browser创建任务,而无须通过行为树编辑器。创建新的Blueprint Class类,并将BTTask_BlueprintBase作为其父类。
将新建类命名为BTTask_Attack并打开,添加Event Receive Execute AI节点,这个节点会在行为树激活BTTask_Attack时触发执行。
首先,你需要让蘑菇执行攻击行为。BP_Muffin包含一个IsAttacking变量,当变量设置为true时,蘑菇会执行一次攻击,因此我们添加如下高亮节点:
如果这个任务节点在这里就结束了,那行为树执行就会卡在这个节点上,因为行为树并不知道该节点已执行完毕了,所以我们要在节点链末端添加Finish Execute节点。
接着,启用Success,由于我们用的是Sequence,这样就能让BTTask_Attack的后续节点得以执行。
现在图表看起来应该是这样的:
小结:
- 当行为树激活BTTask_Attack节点时,Event Receive Execute AI节点就会一同触发执行。
- Cast To BP_Muffin节点会检查Controlled Pawn是否为BP_Muffin类型
- 如果是,则设置IsAttacking变量为true
- 通过Finish Execute节点退出当前节点,让行为树继续往下执行
点击Compile并关闭BTTask_Attack。
现在,我们需要将BTTask_Attack节点添加到行为树中。
行为树添加攻击行为
打开BT_Muffin,随后,将BTTask_Attack节点添加到Sequence节点后面。
接着,将Wait节点添加到Sequence节点后面,并将Wait Time设置为2。确保蘑菇小人不会攻击个不停。
回到主编辑器点击Play运行游戏,像上次一样生成两个蘑菇小人。蘑菇小人会朝着敌人走去。随后,它会尝试攻击,然后休息两秒。当它发现另一个敌人时,又会重复以上行为。
在最后一部分,我们要将攻击和移动两颗子树合并在一起。
合并子树
为了合并子树,我们要用上Selector组合节点。类似于Sequence节点,它也是按从左向右的顺序执行的。然而,Selector节点会在子节点返回成功而非失败时停止执行。利用这个特性,就可以确保行为树每次只执行一颗子树。
打开BT_Muffin并在Root节点下创建Selector节点。随后,如下图连接两个子树:
这样同一时间只有一颗子树会得到执行,下面是每颗子树的执行情况:
攻击: Selector节点会首先运行第一颗子树,如果所有任务都成功了,Sequence节点也会返回执行成功。Selector节点得知执行成功,就会停止执行后面的节点,这样就不会再执行移动节点。
移动: Selector节点会尝试运行前面的攻击子树,如果Enemy还没有值,MoveTo节点就会执行失败,Sequence节点也就同样失败。由于第一个子树失败了,Selector节点就会执行后续这颗移动子树。
回到主编辑器,按下Play运行游戏,生成一些蘑菇小人试试看吧!
“等等,为什么图中这个蘑菇小人没有马上攻击另一只呢?”
在传统的行为树设计里,行为树每帧都会从根节点开始执行,意味着每帧更新,它都会尝试执行第一颗攻击子树,然后再执行第二颗移动子树,这也意味着当Enemy值发生变化时,行为树就会马上切换执行另一颗子树。
然而,Unreal的行为树并不是这样设计执行的。在Unreal里,行为树会继续执行上一帧选中的那颗子树。图中由于AI感知没有马上感知到另一只蘑菇小人的存在,行为树开始执行移动子树,于是行为树就只能乖乖等待移动子树执行完毕,才能重新评估确定执行攻击子树。
为了解决这个问题,我们需要用上最后一种类型节点:装饰(decorators)节点。
创建装饰节点
类似于服务节点,装饰节点也依附于任务或组合节点。通常而言,装饰节点用于做前置检查。如果检查结果为true,装饰节点就返回true,反之亦然。通过装饰节点,就能控制其依附节点是否能够执行。
装饰节点也有能力中止子树的运行,这意味着我们能实现一旦Enemy有设值,就立即中止移动子树。这样蘑菇小人就能在发现敌人的第一时间攻击敌人。
要实现中止功能,我们可以使用Blackboard装饰节点,这个节点只是简单地检查某个黑板键值是否有值。打开BT_Muffin,并在攻击子树的Sequence节点点击右键,从弹出菜单选中Add Decorator\Blackboard,这样Sequence节点就会添加上Blackboard节点。
接着,选中Blackboard装饰节点,并在Details面板将Blackboard Key设为Enemy。
这样可以判断Enemy是否有值,如果没有值,节点返回失败,从而导致Sequence失败,从而让移动子树得到执行。
为了中止移动子树,我们需要用上Observer Aborts设置。
使用Observer Aborts
Observer Aborts能够实现所选中的黑板键值发生变化时,中止执行子树,这里分为两种类型的中止:
- Self: 该设置允许当Enemy值失效时,立即中止运行攻击子树,这种情况发生在攻击子树还未运行完毕,而Enemy又死亡的时候。
- Lower Priority:该设置允许当Enemy有值时,中止运行较低优先度的子树。由于移动子树放在攻击子树后面,它就是较低优先度子树。
我们将Observer Aborts设为Both,同时启用两种类型的中止。
现在,当AI已经没有敌人目标时,可以马上从攻击子树切换运行移动子树。同样的,当AI检测到敌人目标时,又能从移动子树切换运行攻击子树。
以下是完整的行为树图表:
攻击子树小结:
- 当Enemy有值,Selector开始运行攻击子树
- 一旦运行子树,角色开始朝敌人走去
- 随后,进行攻击
- 最后,角色原地停留2秒
移动子树小结:
- 当Enemy没有值,攻击子树运行失败时,Selector继续运行移动子树
- BTService_SetRandomLocation生成一个随机位置
- 角色朝指定位置移动
- 随后,角色原地停留5秒
关闭BT_Muffin并按下Play运行游戏,生成一些蘑菇小人进行一场你死我活的决斗吧!
后续学习
你可以在这里下载完整项目。
如你所见,制作简单AI还算一件不难的事。如果你想创建一个更加高级的AI,请查阅场景查询系统,这个系统允许AI收集场景数据并作出相应的反馈。
如果你还想继续学习引擎其他内容,点击下篇教程,将教你如何制作一个简单的第一人称射击游戏。