Bỏ qua để đến nội dung

Callbacks

Phần này đề cập đến việc lấy dữ liệu, tạo các hàm hiệu ứng và sử dụng lớp Meter để kích hoạt các hàm đó. Phần này sẽ không giải thích chi tiết về animation hay các trình xử lý hiệu ứng. Nếu bạn muốn ôn lại, tôi khuyên bạn nên xem các phần Hướng dẫn hoặc Tham chiếu API của chúng tôi.

Quy trình kích hoạt animation hiệu quả từ đầu đến cuối là:

  1. Sử dụng phương thức Meter.setValue(value) bên trong hàm update để đưa dữ liệu thô của meter vào instance lớp Meter của bạn.
  2. Khi Meter đánh giá rằng dữ liệu ổn định, nó kích hoạt hàm callback được truyền vào làm tham số thứ hai.
  3. Hàm callback này chứa logic điều kiện để đánh giá trạng thái của Meter. Nếu trạng thái là đúng, hiệu ứng nên được thêm vào mảng hiệu ứng (effects.push(new yourEffect())) hoặc vào trình xử lý trạng thái (stateMgr.Push(new yourEffect())).
  4. Mỗi lượt của hàm update đánh giá toàn bộ mảng hiệu ứng và phần tử mới nhất trong trình xử lý trạng thái. Ở giai đoạn này, các hiệu ứng được vẽ dựa trên các biến trạng thái.
  5. Sau khi vẽ, mỗi hiệu ứng đóng góp vào việc kiểm tra vòng đời của nó. Các hiệu ứng trong mảng hiệu ứng chỉ đơn giản lặp biến vòng đời được đặt trên instance lớp của chúng. Khi vòng đời đạt 0 hoặc thấp hơn, hiệu ứng được loại bỏ nhẹ nhàng bằng splice trong quá trình đánh giá mảng hiệu ứng. Các hiệu ứng trong trình xử lý trạng thái có thể tự loại bỏ theo nhu cầu, vì trình xử lý trạng thái hoạt động như một stack và logic điều kiện cho các hiệu ứng đó thường phức tạp hơn.

Meter này tìm kiếm thanh sinh mạng điển hình và trả về tỷ lệ phần trăm màu tương ứng dưới dạng số từ 0 đến 1. Phạm vi HSL trong trường hợp này khá rộng để tính đến gradient trong màu sắc của thanh. Điều này gây ra vấn đề: khi thanh sinh mạng giảm, nó lộ ra nền trong suốt có thể chứa các yếu tố màu xanh từ môi trường trò chơi, vượt qua kiểm tra màu và làm nhiễu loạn meter của chúng ta.

<head>
<meta meter="health" tags="example" x= ".05" y=".9" width=".189" h="70-140" s="40-100" l="40-100" type="linear"/>
</head>

Ở đây tôi đã tạo một instance của lớp Meter có tên healthM để kích hoạt hiệu ứng “nhận sát thương”.

<script>
// Meter instance
var healthM = new Meter(25, healthHelper);
function update () => {
healthM.setValue(health);
window.requestAnimationFrame(update);
}
function healthHelper () => {
// "Took Damage" effect up next
}
// . . .
</script>

Tôi đã chọn yêu cầu ổn định khá cao (25) để tính đến vấn đề bất ổn đã đề cập ở trên. Hàm callback đính kèm chỉ kích hoạt khi giá trị meter ổn định trong 25 lần cập nhật, loại bỏ hầu hết dữ liệu không phải thanh sinh mạng thực sự, ngay cả khi người chơi đứng yên. Nhược điểm duy nhất là độ trễ hiệu ứng ngày càng tăng, trong tình huống này sẽ luôn là yếu tố hạn chế. Đối với thanh sinh mạng cụ thể này, chúng ta có thể giảm thiểu điều đó theo ít nhất một vài cách:

  • Khi đòn đánh trúng, một phần của thanh xanh ngay lập tức chuyển thành đỏ trước khi thu nhỏ về giá trị xanh mới. Nếu chúng ta thêm meter healthRed tìm kiếm màu đỏ ở cùng vị trí với meter sinh mạng, chúng ta có thể kết hợp các giá trị của chúng để kích hoạt nhanh hơn. Điều kiện if ( healthM.decreased && healthRed.increased ) nên cung cấp độ chính xác tốt trong trường hợp này.
  • Chúng ta cũng có thể kết hợp các kết quả đọc từ nhiều meter tuyến tính với cùng cài đặt màu. Hiện tại tôi có một meter tuyến tính ở giữa thanh sinh mạng. Nếu tôi cài đặt thêm hai meter, một cái ngay trên và một cái dưới bản gốc, nhưng vẫn nằm trong thanh sinh mạng, sự gần nhau của ba giá trị meter có thể được sử dụng như một kiểm tra ổn định bổ sung. Cả ba meter sẽ phải báo cáo Meter.decreased và nằm trong phạm vi nhỏ của giá trị nhau để kích hoạt hiệu ứng.

Hàm callback này chỉ thực thi sau khi meter xác nhận rằng tất cả các phần tử trong mảng giá trị của nó đều bằng nhau. Điều này đảm bảo rằng các giá trị meter dựa trên dữ liệu ổn định. Bên trong callback, tôi sử dụng câu lệnh điều kiện để xác định xem có nên phát hiệu ứng hay không.

let healthPrev = 0;
function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
effects.push(new healthEffect());
healthPrev = healthM.value;
}
}

Câu lệnh điều kiện yêu cầu hai điều kiện đúng: sự giảm meter ổn định và sự khác biệt giữa giá trị meter hiện tại và giá trị đã ghi trước đó. Tôi đã thêm điều kiện thứ hai này để tránh lỗi. Nếu chúng ta chỉ kiểm tra sự giảm, hiệu ứng sẽ phát đi phát lại. Vì Meter.decreased được đặt khi meter trở nên ổn định, giá trị của nó vẫn giữ nguyên cho đến khi meter thay đổi. Theo cách này, hiệu ứng chỉ phát khi có sự giảm khác với giá trị ổn định cuối cùng.

Tôi tạo hai hiệu ứng: một cho mảng hiệu ứng và một cho trình xử lý trạng thái.

Khối code đầu tiên này định nghĩa hàm hiệu ứng dành cho mảng hiệu ứng. Nó được thiết kế để nhanh và nhẹ, để nhiều hiệu ứng nhỏ hơn có thể thực thi đồng thời. Để giảm tải hệ thống, tôi đã làm cho hiệu ứng hơi linh hoạt và có thể tái sử dụng thông qua các tham số.

function healthEffect(color, direction, speed){
// The lifetime of the effect should be iterated in the draw function.
this.lifetime = 200;
this.speed = speed;
this.direction = direction;
this.color = color;
this.draw = () => {
// Set fill from parameter
ctx.fillStyle = `hsl(${this.color}, 100%, 50%)`;
// Check direction
if (this.direction == "up"){
ctx.fillRect(0, this.lifetime, width, 30);
} else {
ctx.fillRect(0, height - this.lifetime, width, 30);
}
// As the lifetime decreases by 5 each update, the rectangle moves accordingly.
this.lifetime -= speed;
}
}
function update() {
// Background color
ctx.fillStyle = "black";
ctx.fillRect(0, 0, 320, 200);
// Draw all effects in array and remove them if they are done.
effects.forEach((ele, i) => {
ele.draw();
if (ele.lifetime == 0){
effects.splice(i, 1);
}
})
window.requestAnimationFrame(update);
}
function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
// Make sure to correctly set the color and direction paremeters.
effects.push(new healthEffect(1, "down", 5));
healthPrev = healthM.value;
}
}

Đây là cách nó trông trong thực tế:

Nhờ thiết kế linh hoạt của hàm, chúng ta có thể dễ dàng tạo hiệu ứng hồi máu! Chỉ cần thêm điều kiện bổ sung vào hàm callback.

function healthHelper () => {
if (healthM.decreased && healthM.value != healthPrev){
// Damage effect
effects.push(new healthEffect(1, "down", 5));
healthPrev = healthM.value;
}
if (healthM.increased && healthM.value != healthPrev){
// Healing effect
effects.push(new healthEffect(120, "up", 5));
healthPrev = healthM.value;
}
}

Sự đơn giản của hiệu ứng này là lợi thế lớn nhất của nó đối với nhà phát triển LightScript. Thay vì tạo các hiệu ứng thiết kế tùy chỉnh mỗi cái yêu cầu hàng trăm dòng code, bạn xây dựng các hiệu ứng nhỏ, có thể tái sử dụng phục vụ như các khối xây dựng cho các bộ kích hoạt cụ thể. Ba hiệu ứng tiếp theo là các biến thể dựa trên hiệu ứng nhỏ mà chúng ta vừa tạo. Hãy thử nghiệm và xem bạn có thể đạt được gì!

Cá nhân tôi dành trình xử lý trạng thái cho các hiệu ứng lớn, phức tạp với logic trạng thái rất cụ thể, đặc biệt khi hiệu ứng cần ưu tiên hơn tất cả các hiệu ứng khác. Lưu ý rằng trình xử lý trạng thái hoạt động như một stack và chỉ thực thi phần tử đầu; mỗi hiệu ứng trạng thái chịu trách nhiệm tự loại bỏ khi hoàn thành. Ví dụ này minh họa animation lobby matchmaking thích nghi tùy thuộc vào việc bạn có đang tìm kiếm hay không. Nó cũng chứa mảng hiệu ứng nhỏ của riêng nó với hiệu ứng trợ giúp. Các meter được sử dụng ở đây là inLobbyyellowPlayButton, chỉ kích hoạt trong lobby matchmaking. yellowPlayButton thay đổi màu tùy thuộc vào việc bạn có đang tìm kiếm hay không.

function lobbyAnimation(){
// Instance variables
this.start = new Date().getTime();
this.elapsed = 0;
this.radius = 0;
this.searching = 1000;
// Instance effects array
this.effects = [];
// Process is the method that allows a state effect to remove itself
this.Process = function () {
// If the lobby menu and play button disappear, remove the effect
if (inLobby.decreased && yellowPlayButton.decreased) {
stateMgr.Pop();
// Global boolean to prevent duplicate lobby effects in the state handler
lobbyAnim = false;
}
// If the play button is present, edit this.searching based on its color
yellowPlayButton.value == 1 ? this.searching = 1000 : this.searching = 100;
// Call the Draw function attached to the effect
this.Draw();
};
this.Draw = function(){
// Iterate important variables
this.elapsed = new Date().getTime() - this.start;
this.radius = 50 + (Math.sin(this.elapsed/this.searching)*10);
// Fill the background to overwrite other effects
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, 320, 200);
// Draw the main arc
ctx.beginPath();
ctx.strokeStyle = `hsl(120, 100%, 50%)`;
ctx.lineWidth = 20;
ctx.arc(160, 100, this.radius, 0, 2 * Math.PI);
ctx.stroke();
// If the main arc is the right size, push helper effects into the helper array
if(this.radius > 59 && this.elapsed % 2 == 0){
this.effects.push(new lobbyHelper(160, 100, 69));
}
// Play and remove helper effects if necessary
this.effects.forEach((e, i) => {
ctx.lineWidth = 1;
e.draw();
if (e.radius > 360) {
this.effects.splice(i, 1);
}
});
}
}
function lobbyHelper(x, y, radius){
this.x = x;
this.y = y;
this.radius = radius;
this.draw = function(){
// Set the global transparency value. 1 is opaque, 0 is invisible.
ctx.globalAlpha = 1 - this.radius/360;
ctx.beginPath();
ctx.strokeStyle = `hsl(120, 100%, ${50 + (this.radius/7)}%)`;
ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
ctx.stroke();
// Set the global transparency back to 1 so your other effects are opaque
ctx.globalAlpha = 1;
this.radius++;
}
}
// Declare state handler (found in Snippets)
var stateMgr = new StateHandler();
// Global boolean to prevent duplicate lobby animations
var lobbyAnim = false;
function update(){
// Run the Process function to draw the animation
stateMgr.Process();
}
function lobbyAnimationPlay(){
if(inLobby.value == 1 && yellowPlayButton.value == 1 && !lobbyAnim){
stateMgr.Push(new lobbyAnimation());
lobbyAnim = true;
}
};

Không tìm kiếm:

Đang tìm kiếm:

Với đó, chúng ta hoàn thành hướng dẫn Callbacks, cũng như hướng dẫn nhà phát triển chính thức của SignalRGB. Hãy theo dõi phần bổ sung tiếp theo và cuối cùng trong hướng dẫn của chúng tôi, Bảo trì Lightscript!