Реализация игрового процесса
The primary objective of the game is to drive through a race track comprising several checkpoints within a specified time. To implement such a game, we also need to consider several other essential components:Главная идея игры в том, чтобы проехать трассу, состоящую из нескольких контрольных точек за отведенное время. Разберем подробнее, что нам еще понадобится для реализации такой игры:
- Minimap to mark the player and checkpointsМиникарта для обозначения игрока и контрольных точек;
- Checkpoint entity and race track entity consisting of several checkpointsСущность контрольной точки и трассы, составленной из нескольких точек;
- Lap timerТаймер для отсчета времени прохождения трассы;
- Simple interface displaying statisticsПростой интерфейс, отображающий статистику;
- End game screenОкно завершения заезда;
- Game managing componentГлавный компонент управления игрой.
Implementing a Minimap for the LocationРеализация миникарты локации#
The minimap should be located in the corner of the screen and display the player's position and orientation and the next checkpoint location. Let's create the Map component for this purpose. We'll use the WidgetSprite widget that allows displaying multiple image layers (to draw the checkpoints and the player's position without changing the map itself). Миникарта должна находиться в углу экрана и отображать позицию и ориентацию игрока и следующую контрольную точку на карте локации. Создадим компонент Map для этого. Воспользуемся виджетом WidgetSprite, позволяющим отображать несколько слоев с изображениями (чтобы иметь возможность отрисовывать точки и положение игрока, не трогая саму карту).
Map.csMap.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
#region Math Variables
#if UNIGINE_DOUBLE
using Vec3 = Unigine.dvec3;
#else
using Vec3 = Unigine.vec3;
#endif
#endregion
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
public class Map : Component
{
// компонент представлен синглтоном для глобального доступа
private static Map instance = null;
public class MapMarker { }
// абстракция Marker представляет собой маркер на карте с информацией о слое, которому он принадлежит
private class Marker : MapMarker
{
private int layer = -1;
protected Map owner = null;
public Marker(Map owner, int layer)
{
this.owner = owner;
this.layer = layer;
}
public virtual void Update() { }
public void Clear()
{
owner.mapSprite.RemoveLayer(layer);
owner = null;
layer = -1;
}
public void DecLayer(int removed_layer)
{
if (layer <= removed_layer)
return;
layer--;
}
public int Layer { get { return layer; } }
}
// обычные точечные маркеры будут использоваться для отображения контрольных точек
private class PointMarker : Marker
{
public PointMarker(Map owner, int layer, Vec3 point) : base(owner, layer)
{
ivec2 image_position = base.owner.GetImagePosition(point);
base.owner.mapSprite.SetLayerTransform(Layer, MathLib.Translate(image_position));
}
}
// данный маркер будет представлять игрока и его направление на миникарте
private class CarMarker : PointMarker
{
private Car car;
private int imageSize = 0;
public CarMarker(Map owner, int layer, Car car,int image_size) : base(owner, layer, car.node.WorldPosition)
{
imageSize = image_size;
this.car = car;
}
// поворот спрайта маркера в зависимости от вращения ноды с компонентом Car
public override void Update()
{
Vec3 position = car.node.WorldPosition;
ivec2 image_position = owner.GetImagePosition(position);
vec3 direction = car.node.GetWorldDirection(MathLib.AXIS.Y);
direction.z = 0.0f;
float a = MathLib.AngleSigned(direction, vec3.FORWARD, vec3.UP);
int hsize = imageSize / 2;
mat4 rot = MathLib.Translate(hsize,hsize,0.0f) * MathLib.RotateZ(a) * MathLib.Translate(-hsize,-hsize,0.0f);
owner.mapSprite.SetLayerTransform(Layer, MathLib.Translate(image_position) * rot);
}
}
// определим пути к изображениям для фона, контрольной точки и автомобиля
[ShowInEditor, ParameterFile] private string heigthMapImage = "";
[ShowInEditor, ParameterFile] private string pointMarkerImage = "";
[ShowInEditor, ParameterFile] private string carMarkerImage = "";
// получим ссылку на главный слой террейна, чтобы выяснить его пространственные границы
[ShowInEditor] private LandscapeLayerMap layerMap = null;
// Используем WidgetSprite для миникарты -- виджет, позволяющий иметь несколько слоев с изображениями
private WidgetSprite mapSprite = null;
// сами изображения
private Image mapImage = null;
private Image pointImage = null;
private Image carImage = null;
// список маркеров на карте
private List<Marker> markers = new List<Marker>();
// метод для преобразования мировых координат в координаты миникарты
private ivec2 GetImagePosition(Vec3 world_coord)
{
int w = MathLib.CeilToInt(world_coord.x * mapImage.Width / layerMap.Size.x);
int h = MathLib.CeilToInt(mapImage.Height - world_coord.y * mapImage.Height / layerMap.Size.y);
return new ivec2(w, h);
}
private void Init()
{
Gui gui = Gui.GetCurrent();
mapImage = new Image(heigthMapImage);
pointImage = new Image(pointMarkerImage);
carImage = new Image(carMarkerImage);
mapSprite = new WidgetSprite(gui);
mapSprite.SetImage(mapImage);
gui.AddChild(mapSprite, Gui.ALIGN_RIGHT);
int map_size = MathLib.CeilToInt(gui.Width * 0.15f);
mapSprite.Height = map_size;
mapSprite.Width = map_size;
instance = this;
}
private void Update()
{
foreach(Marker m in markers)
{
m.Update();
}
}
private void Shutdown()
{
for (int i = markers.Count - 1; i >= 0; i--)
{
RemoveMarker(markers[i]);
}
markers.Clear();
Gui gui = Gui.GetCurrent();
gui.RemoveChild(mapSprite);
mapSprite.DeleteLater();
}
private MapMarker AddPointMarker(Vec3 point)
{
int layer = mapSprite.AddLayer();
mapSprite.SetLayerImage(layer, pointImage);
PointMarker m = new PointMarker(this, layer, point);
markers.Add(m);
return m;
}
private MapMarker AddCarMarker(Car car)
{
int layer = mapSprite.AddLayer();
mapSprite.SetLayerImage(layer, carImage);
CarMarker m = new CarMarker(this, layer, car, carImage.Width);
markers.Add(m);
return m;
}
// для удаления маркера требуется удалить слой из списка
// и корректно сдвинуть индексы слоя в оставшихся маркерах
private void RemoveMarker(MapMarker marker)
{
Marker m = marker as Marker;
if (m == null)
return;
foreach(Marker mark in markers)
{
mark.DecLayer(m.Layer);
}
m.Clear();
markers.Remove(marker as Marker);
}
// статические методы синглтона для глобального доступа извне
public static MapMarker CreatePointMarker(Vec3 point)
{
if (instance == null)
return null;
return instance.AddPointMarker(point);
}
public static MapMarker CreateCarMarker(Car car)
{
if (instance == null)
return null;
return instance.AddCarMarker(car);
}
public static void ClearMarker(MapMarker marker)
{
if (instance == null)
return;
instance.RemoveMarker(marker);
}
}
Let's create one more NodeDummy for the Map component and set the images for the background and markers and the LandscapeLayerMap node — the terrain layer.Создадим еще один NodeDummy для компонента Map и укажем изображения для фона и маркеров и ноду LandscapeLayerMap — слой террейна.
Implementing the Race Track and CheckpointsРеализация трассы и контрольных точек#
The CheckPoint component will represent a checkpoint of the race track that the player needs to pass. To detect the moment of passing, we use Physical Trigger that triggers a callback when physical bodies enter its volume.Компонент CheckPoint будет представлять собой контрольную точку на трассе, которую игроку нужно пройти. Для определения момента прохождения используем физический триггер (Physical Trigger), который вызывает коллбэк при вхождении физических тел в его объем.
CheckPoint.csCheckPoint.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
public class CheckPoint : Component
{
// ссылка на физический триггер
[ShowInEditor] private PhysicalTrigger trigger = null;
// маркер на миникарте, представляющий эту контрольную точку
private Map.MapMarker marker = null;
// коллбэк срабатывания контрольной точки
private Action<Car> callback = null;
// коллбэк срабатывает при посещении контрольной точки автомобилем
private void OnEnter(Body body)
{
if (callback == null)
return;
Car car = body.Object.GetComponent<Car>();
if (car == null)
return;
callback(car);
}
// по умолчанию все контрольные точки должны быть выключены, их переключением будет управлять отдельный компонент
[Method(Order = 0)]
private void Init()
{
SetEnabled(false);
trigger.AddEnterCallback(OnEnter);
}
private void Shutdown()
{
callback = null;
marker = null;
SetEnabled(false);
}
// при включении контрольной точки на миникарту будет добавлен её маркер
// и удален при отключении
public void SetEnabled(bool mode)
{
node.Enabled = mode;
if (mode)
{
if (marker == null)
marker = Map.CreatePointMarker(node.WorldPosition);
}
else
{
if (marker != null)
{
Map.ClearMarker(marker);
marker = null;
}
}
}
public void SetEnterCallback(Action<Car> callback)
{
this.callback = callback;
}
}
Find the race/props/checkpoint/CheckPoint.node asset and add it to the scene by dragging with the left mouse button in the viewport.Найдите ассет race/props/checkpoint/CheckPoint.node и добавьте на сцену, просто перетащив его левой кнопкой мыши во вьюпорт.
This is a Node Reference, click Edit to modify this instance. It has a cylinder and a decal inside to visualize the checkpoint. Our task is to create a new node via the menu Create → Logic → Physical Trigger, make it a child of the point node, and place within the checkpoint bounds by selecting the Cylinder type and setting the required size.Это Node Reference, нажмите Edit, чтобы отредактировать данный экземпляр. Внутри расположены цилиндр и декаль для визуального представления контрольной точки. Наша задача — создать новую ноду в меню Create → Logic → Physical Trigger в качестве дочерней к ноде point и расположить в границах контрольной точки, выбрав тип Cylinder и установив нужные размеры.
Then select the top point node and assign the CheckPoint component to it. Make sure to set the trigger in the component parameters by dragging the child PhysicalTrigger to the corresponding field.Далее, выделите верхнюю ноду point и назначьте на неё компонент CheckPoint. Не забудьте указать триггер в параметрах компонента, перетянув дочерний физический триггер в поле.
After that, save the Node Reference by clicking Apply, then create several instances of the CheckPoint node, and place them on the landscape, mapping out a small track consisting of points.После этого сохраните Node Reference, нажав Apply, затем создайте несколько экземпляров ноды CheckPoint, и расставьте их по ландшафту, организовав небольшой импровизированный трек из точек.
The next step is to create a track from the placed points. First, group them by selecting all checkpoints and pressing Ctrl + G, and rename the added parent NodeDummy from group to Track. Now let's create a new Track component that will receive the list of child nodes with the CheckPoint component, shuffle their order and provide an interface for various actions with the track:Следующим шагом необходимо создать трассу из расставленных точек. Для начала сгруппируем их, выделив все контрольные точки и нажав Ctrl + G, и переименуем добавленный родительский NodeDummy из group в Track. Теперь создадим новый компонент Track, который будет получать список дочерних нод с компонентом CheckPoint, перемешивать их порядок и предоставлять интерфейс для различных действий с трассой:
Track.csTrack.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
#region Math Variables
#if UNIGINE_DOUBLE
using Vec3 = Unigine.dvec3;
#else
using Vec3 = Unigine.vec3;
#endif
#endregion
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
public class Track : Component
{
// список контрольных точек этой трассы
private CheckPoint[] points = null;
private int[] current_track = null;
// индекс текущей точки
private int current_point = -1;
// коллбэки завершения трассы и посещения контрольной точки
private Action track_end_callback = null;
private Action check_point_callback = null;
// метод, вызываемый из компонента контрольной точки при посещении игроком
private void OnCarEnter(Car car)
{
points[current_track[current_point]].SetEnabled(false);
current_point++;
if (current_point < NumPoints)
{
points[current_track[current_point]].SetEnabled(true);
check_point_callback?.Invoke();
}
else
{
current_point = -1;
track_end_callback?.Invoke();
}
}
// список контрольных точек строится на основе дочерних нод с нужным компонентом
[Method(Order = 1)]
private void Init()
{
points = GetComponentsInChildren<CheckPoint>(node);
current_track = new int[points.Length];
for (int i = 0; i < points.Length; i++)
{
points[i].SetEnterCallback(OnCarEnter);
current_track[i] = i;
}
}
private void Shutdown()
{
points = null;
current_track = null;
current_point = -1;
track_end_callback = null;
check_point_callback = null;
}
// каждая новая трасса строится перетасовкой списка контрольных точек
public void Shuffle()
{
if (current_point >= 0 && current_point < NumPoints)
points[current_track[current_point]].SetEnabled(false);
for (int i = current_track.Length - 1; i >= 1; i--)
{
int j = MathLib.ToInt(MathLib.RandInt(0, i + 1));
int tmp = current_track[j];
current_track[j] = current_track[i];
current_track[i] = tmp;
}
current_point = 0;
points[current_track[current_point]].SetEnabled(true);
}
public void Restart()
{
if (current_point >= 0 && current_point < NumPoints)
points[current_track[current_point]].SetEnabled(false);
current_point = 0;
points[current_track[current_point]].SetEnabled(true);
}
public void SetTrackEndCallback(Action callback)
{
track_end_callback = callback;
}
public void SetCheckPointCallback(Action callback)
{
check_point_callback = callback;
}
public int NumPoints { get { return points.Length; } }
public vec3 GetPoint(int num)
{
return (vec3)points[num].node.WorldPosition;
}
// длина трассы, полученная сложением расстояний между контрольными точками и начальной позицией
public double GetLength(Vec3 start_position)
{
double result = MathLib.Length(GetPoint(0) - start_position);
for(int i = 1; i < NumPoints; i++)
{
result += MathLib.Length(GetPoint(current_track[i - 1]) - GetPoint(current_track[i]));
}
return result;
}
}
Assign the Track component to the checkpoints group (the Track node).Назначим компонент Track на группу контрольных точек (нода Track).
Implementing the TimerРеализация таймера#
Let's also add the Timer component that implements a simple timer each frame adding the time spent on rendering the previous frame to the current time value. Thus, we don't need a system timer.Добавим также компонент Timer, реализующий простой таймер, каждый кадр прибавляя к значению текущего времени время, затраченное на отрисовку предыдущего кадра. Таким образом, системные таймер нам не понадобится.
Timer.csTimer.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Unigine;
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
public class Timer : Component
{
private static StringBuilder builder = new StringBuilder();
private Action time_left_callback = null;
private double time = 0.0;
private double timer = 0.0;
private bool is_running = false;
private void Update()
{
if (!is_running)
return;
timer += Game.IFps;
if (timer > time)
{
is_running = false;
time_left_callback?.Invoke();
}
}
public void Start(double time)
{
is_running = true;
this.time = time;
timer = 0.0f;
}
public void Stop()
{
is_running = false;
}
public void Resume()
{
is_running = true;
}
public void SetTimeLeftCallback(Action callback)
{
time_left_callback = callback;
}
public double TimeLeft { get { return time - timer; } }
public double TimePassed { get { return timer; } }
public static string SecondsToString(double seconds)
{
builder.Clear();
double t = seconds;
int h = MathLib.FloorToInt((double)(t / 3600.0));
t -= h * 3600.0;
int m = MathLib.FloorToInt((double)(t / 60.0));
t -= m * 60.0;
int s = MathLib.FloorToInt((double)t);
if (h > 0)
{
builder.Append(h.ToString());
builder.Append("h:");
if(m < 10) builder.Append('0');
}
if (m > 0)
{
builder.Append(m.ToString());
builder.Append("m:");
if(s < 10) builder.Append("0");
}
builder.Append(s.ToString());
builder.Append('s');
return builder.ToString();
}
}
We'll also need a dedicated NodeDummy for the Timer component — let's create it and assign this component to it.Нам также понадобится отдельный NodeDummy для компонента таймера — создадим его и назначим компонент.
Implementing the InterfaceРеализация интерфейса#
A simple interface displaying the current speed, timer, and the best race time would be a nice thing to have. Let's create a separate HUD component for that and add the methods to update the values from outside. Assign the HUD component to a separate NodeDummy node.Простой интерфейс с отображением текущей скорости движения, таймера и лучшего времени прохождения данной трассы не будет лишним. Оформим это в отдельном компоненте HUD, добавив методы для обновления значений извне. Назначим компонент HUD на отдельную ноду NodeDummy.
HUD.csHUD.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
public class HUD : Component
{
[ShowInEditor] private int font_size = 50;
private WidgetLabel speed_label = null;
private WidgetLabel time_label = null;
private WidgetLabel best_time_label = null;
private WidgetVBox vbox = null;
private void Init()
{
Gui gui = Gui.GetCurrent();
vbox = new WidgetVBox(gui);
WidgetLabel label = null;
WidgetGridBox grid = new WidgetGridBox(gui, 2);
label = new WidgetLabel(gui, "Best Time:");
label.FontSize = font_size;
best_time_label = new WidgetLabel(gui, "");
best_time_label.FontSize = font_size;
grid.AddChild(label);
grid.AddChild(best_time_label);
label = new WidgetLabel(gui, "Time:");
label.FontSize = font_size;
time_label = new WidgetLabel(gui, "");
time_label.FontSize = font_size;
grid.AddChild(label);
grid.AddChild(time_label);
label = new WidgetLabel(gui, "Speed:");
label.FontSize = font_size;
speed_label = new WidgetLabel(gui, "");
speed_label.FontSize = font_size;
grid.AddChild(label);
grid.AddChild(speed_label);
vbox.AddChild(grid);
gui.AddChild(vbox, Gui.ALIGN_BOTTOM | Gui.ALIGN_RIGHT | Gui.ALIGN_OVERLAP);
}
private void Shutdown()
{
Gui gui = Gui.GetCurrent();
gui.RemoveChild(vbox);
vbox.DeleteLater();
speed_label = null;
time_label = null;
best_time_label = null;
vbox = null;
}
public void SetSpeed(float speed)
{
speed_label.Text = String.Format("{0} km/h",MathLib.RoundToInt(speed));
}
public void SetTime(double time)
{
time_label.Text = Timer.SecondsToString(time);
}
public void SetBestTime(double time)
{
best_time_label.Text = Timer.SecondsToString(time);
}
public void Show()
{
vbox.Hidden = false;
}
public void Hide()
{
vbox.Hidden = true;
}
}
Implementing the End Game ScreenРеализация финального окна#
One more component (UI) will be responsible for the window with the choice of actions after the player finished driving the track or ran out of time. Its only function is to display a widget with the race result text and two buttons: replay the current track and generate a new one. За финальное окно с выбором действий после прохождения трассы или истечения времени будет отвечать еще один компонент UI. Все, что он выполняет, это отображение виджета в окне с надписью о результате заезда и двумя кнопками: для повторного прохождения текущей трассы и генерации новой трассы.
UI.csUI.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
public class UI : Component
{
public enum Status
{
Win,
Lose,
}
WidgetVBox vbox = null;
WidgetLabel status_label = null;
private Action restart_click_callback = null;
private Action new_click_callback = null;
private void OnRestartClicked() { restart_click_callback?.Invoke(); }
private void OnNewClicked() { new_click_callback?.Invoke(); }
private void Init()
{
Gui gui = Gui.GetCurrent();
vbox = new WidgetVBox(gui);
int vbox_size = 100;
vbox.Width = vbox_size;
vbox.Height = vbox_size;
vbox.Background = 1;
status_label = new WidgetLabel(gui, "Status");
status_label.TextAlign = Gui.ALIGN_CENTER;
WidgetSpacer spacer = new WidgetSpacer(gui);
WidgetButton button_restart = new WidgetButton(gui, "Restart");
button_restart.Toggleable = false;
WidgetButton button_new = new WidgetButton(gui, "New Track");
button_new.Toggleable = false;
button_restart.AddCallback(Gui.CALLBACK_INDEX.CLICKED, OnRestartClicked);
button_new.AddCallback(Gui.CALLBACK_INDEX.CLICKED, OnNewClicked);
vbox.AddChild(status_label);
vbox.AddChild(spacer);
vbox.AddChild(button_restart);
vbox.AddChild(button_new);
vbox.Arrange();
gui.AddChild(vbox, Gui.ALIGN_OVERLAP);
vbox.SetPosition(gui.Width / 2 - vbox_size / 2, gui.Height / 5);
Hide();
}
private void Shutdown()
{
Gui gui = Gui.GetCurrent();
gui.RemoveChild(vbox);
vbox.DeleteLater();
vbox = null;
status_label = null;
restart_click_callback = null;
new_click_callback = null;
}
public void Show(Status status)
{
status_label.Text = status == Status.Win ? "You Win" : "You Lose";
vbox.Hidden = false;
ControlsApp.MouseHandle = Input.MOUSE_HANDLE.USER;
}
public void Hide()
{
vbox.Hidden = true;
ControlsApp.MouseHandle = Input.MOUSE_HANDLE.GRAB;
}
public void SetRestartClickCallback(Action callback)
{
restart_click_callback = callback;
}
public void SetNewClickCallback(Action callback)
{
new_click_callback = callback;
}
}
This component should also be assigned to a dedicated NodeDummy.Нам также понадобится назначить данный компонент на отдельную ноду NodeDummy.
Implementing the GameplayРеализация игрового процесса#
We finally got to the main GameManager component, which ties together all the game logic and sets the game rules. It controls the timer, the track, the interface, and the end game screen.Наконец, создадим главный компонент GameManager, связывающий воедино всю игровую логику и устанавливающий правила игры. Он управляет таймером, трассой, интерфейсом и финальным окном.
Let's add one more standard speed parameter to calculate the standard time for the generated tracks, this time will be considered the best until the result is updated by the player.Добавим еще один параметр стандартной скорости для расчета стандартного времени прохождения сгенерированных трасс, полученное время будет считаться лучшим, пока результат не будет обновлен игроком.
GameManager.csGameManager.cs
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
#region Math Variables
#if UNIGINE_DOUBLE
using Vec3 = Unigine.dvec3;
using Mat4 = Unigine.dmat4;
#else
using Vec3 = Unigine.vec3;
using Mat4 = Unigine.mat4;
#endif
#endregion
[Component(PropertyGuid = "AUTOGENERATED_GUID")] // <-- идентификатор генерируется автоматически для нового компонента
public class GameManager : Component
{
// ссылки на компоненты игровой логики
[ShowInEditor] private CarPlayer player = null;
[ShowInEditor] private Timer timer = null;
[ShowInEditor] private Track track = null;
[ShowInEditor] private UI ui = null;
[ShowInEditor] private HUD hud = null;
// скорость в км/ч для расчета стандартного времени прохождения трассы
[ShowInEditor] private float default_speed = 1.0f;
// переменная для хранения лучшего результата
private double best_time = 0.0;
// позиция для сброса положения автомобиля и позиция начала трассы
private Mat4 reset_transform = Mat4.IDENTITY;
private Mat4 start_track_transform = Mat4.IDENTITY;
// окно проигрыша при истечении таймера
private void OnTimeLeft()
{
timer.Stop();
ui.Show(UI.Status.Lose);
InputController.IsEnabled = false;
}
// окно выигрыша при завершении трассы
private void OnTrackEnd()
{
timer.Stop();
best_time = timer.TimePassed;
ui.Show(UI.Status.Win);
InputController.IsEnabled = false;
}
// обновляем позицию сброса автомобиля на каждой контрольной точке
private void OnCheckPoint()
{
reset_transform = player.node.WorldTransform;
}
[Method(Order = 2)]
private void Init()
{
reset_transform = player.node.WorldTransform;
Map.CreateCarMarker(player);
ui.SetNewClickCallback(StartTrack);
ui.SetRestartClickCallback(RestartTrack);
timer.SetTimeLeftCallback(OnTimeLeft);
best_time = 0.0;
track.SetTrackEndCallback(OnTrackEnd);
track.SetCheckPointCallback(OnCheckPoint);
StartTrack();
}
private void Update()
{
if (InputController.GetAction(InputController.InputActionType.Reset) > 0.0f)
player.Reset(reset_transform);
// обновление отображения скорости и времени
hud.SetSpeed(player.Speed);
hud.SetTime(timer.TimePassed);
}
private void Start()
{
reset_transform = start_track_transform;
timer.Start(best_time);
hud.SetBestTime(best_time);
ui.Hide();
InputController.IsEnabled = true;
}
// при старте трассы производим расчет стандартного времени прохождения трассы
private void StartTrack()
{
start_track_transform = player.node.WorldTransform;
track.Shuffle();
double length = track.GetLength(player.node.WorldPosition);
double default_time = length / default_speed * 3.6f;
best_time = default_time;
Start();
}
private void RestartTrack()
{
player.Reset(start_track_transform);
track.Restart();
Start();
}
}
Now assign the component to a dedicated node and set the links to the corresponding components by dragging the nodes to which they are assigned into the corresponding fields in the Parameters window.Назначим компонент отдельной ноде и укажем ссылки на соответствующие компоненты, перетаскивая ноды, на которые они назначены в соответствующие поля в окне Parameters.
Set moderate standard speed for the timer, taking into account that one needs to get used to driving on mountainous terrain.Установим не слишком высокую стандартную скорость для таймера, учитывая, что к езде по гористой местности еще необходимо привыкнуть.
To simplify customization of rendering settings for beginners, the template_simplified.node preset with optimal settings has been added to the race folder. To apply it, double-click on the asset, or go to the Settings window, select any subsection of the Render section and select the desired preset in the Render Preset parameter.Чтобы упростить настройку параметров рендера для помощи новичкам, в папку race был добавлен пресет template_simplified.node с оптимальными настройками. Чтобы его применить, щелкните по нему дважды левой кнопкой мыши или перейдите в окно Settings, выберите любой подраздел в Render и выберите нужный пресет в параметре Render Preset.
Done, now you can build the project and enjoy the game!Готово, теперь можно переходить к сборке проекта и наслаждаться игрой!