-
Технико-экономические показатели
В данном приложении возможна внутренняя коммерция, то есть продажа уровней игры, либо дополнительных средств. Средство рекламы не были включены, так как являются не обязательными.
-
Порядок контроля и приемки
Контроль и приемка разработки осуществляются на основе испытаний контрольно-отладочных примеров. При этом проверяется выполнение всех функций программы. Осуществление проверки возможно либо на телефоне, либо на эмуляторе.
Логическая Блок Схема

Описание Программы
-
Структура программы
Программа состоит из 6 сцен с расширением и соответственно нескольких сцен с самими уровнями. Так же существуют файлы с расширением .cs на виртуальном хостинге.
В программе существуют файлы формата fbx, которые представляют собой 3d модели объектов, а так же есть файлы текстур, накладываемые на объекты. Все файлы используются при сборке проекта.
2) Рассмотрим первую сцену – MainMenu.

Мы можем видеть, что главное меню представляет собой лесную поляну с инопланетной тарелкой. Есть 3 кнопки: New Game, Options, Shop и Exit.
Всего у нас 3 сцены в этом файле, это была только часть меню, скриншоты остальных:

Вся сцена управляется одним скриптом, написанном на языке JavaScript. Суть управления состоит в том, что у нас есть 3 сцены и к каждой сцене прикреплена камера, в скрипте мы в зависимости от нажатой кнопки переключаемся между камерами.
var isNewGameButton = false;
var isOptionButton = false;
var isShopButton = false;
var isBackButton = false;
var isQuitButton = false;
var isCampaignButton = false;
var isMultiplayerButton = false;
var isProfileButton = false;
var isBackMultiplayerButton = false;
var isInopButton = false;
var camera1 : Camera;
var camera2 : Camera;
var camera3 : Camera;
function OnMouseEnter() {
renderer.material.color = Color.gray;
}
function OnMouseExit() {
renderer.material.color = Color.white;
}
function OnMouseUp() {
if ( isQuitButton) {
Application.Quit();
}
else if ( isNewGameButton) {
camera1.enabled = false;
camera2.enabled = true;
camera3.enabled = false;
}
else if ( isBackButton) {
Application.LoadLevel (0);
}
else if ( isCampaignButton) {
Application.LoadLevel(1);
}
else if ( isMultiplayerButton) {
camera1.enabled = false;
camera2.enabled = false;
camera3.enabled = true;
}
else if ( isBackMultiplayerButton) {
camera1.enabled = false;
camera2.enabled = true;
camera3.enabled = false;
}
}
3) Рассмотрим непосредственно сцену гонки. Данная сцена осуществляется в режиме одиночной компании. У нас есть машина медведей, которой мы и управляем. Для нас доступны данные о скорости, уровне жизни, круге и времени круга.


Обратимся непосредственно к трассе, она(как и остальные сцены для гонок), была сделана в unity3d, путём манипуляций со стандартными элементами, а так же некоторые элементы были специально дорисованы в 3dmax.

4) Рассмотрим транспортные средства медведей. Каждое транспортное средство представляет собой связь из двух частей, это сама модель, сделанная в 3dmax, а так же управляющий ей скрипт. Каждая модель переносится в unity в формате .fbx. Далее на неё накладывается так называемый collider, который необходим для взаимодействия и представляет собой прозрачную сетку.

Машина медведей с наложенным collider.
Далее приводится код скрипта carcontroller, который отвечает за движение машины. Скрипт написан на языке C# в MonoDevelop.
using UnityEngine;
using System.Collections;
// This class is repsonsible for controlling inputs to the car.
// Change this code to implement other input types, such as support for analogue input, or AI cars.
[RequireComponent (typeof (Drivetrain))]
public class CarController : MonoBehaviour {
// Add all wheels of the car here, so brake and steering forces can be applied to them.
public Wheel[] wheels;
// A transform object which marks the car's center of gravity.
// Cars with a higher CoG tend to tilt more in corners.
// The further the CoG is towards the rear of the car, the more the car tends to oversteer.
// If this is not set, the center of mass is calculated from the colliders.
public Transform centerOfMass;
// A factor applied to the car's inertia tensor.
// Unity calculates the inertia tensor based on the car's collider shape.
// This factor lets you scale the tensor, in order to make the car more or less dynamic.
// A higher inertia makes the car change direction slower, which can make it easier to respond to.
public float inertiaFactor = 1.5f;
// current input state
float brake;
float throttle;
float throttleInput;
float steering;
float lastShiftTime = -1;
float handbrake;
// cached Drivetrain reference
Drivetrain drivetrain;
// How long the car takes to shift gears
public float shiftSpeed = 0.8f;
// These values determine how fast throttle value is changed when the accelerate keys are pressed or released.
// Getting these right is important to make the car controllable, as keyboard input does not allow analogue input.
// There are different values for when the wheels have full traction and when there are spinning, to implement
// traction control schemes.
// How long it takes to fully engage the throttle
public float throttleTime = 1.0f;
// How long it takes to fully engage the throttle
// when the wheels are spinning (and traction control is disabled)
public float throttleTimeTraction = 10.0f;
// How long it takes to fully release the throttle
public float throttleReleaseTime = 0.5f;
// How long it takes to fully release the throttle
// when the wheels are spinning.
public float throttleReleaseTimeTraction = 0.1f;
// Turn traction control on or off
public bool tractionControl = true;
// These values determine how fast steering value is changed when the steering keys are pressed or released.
// Getting these right is important to make the car controllable, as keyboard input does not allow analogue input.
// How long it takes to fully turn the steering wheel from center to full lock
public float steerTime = 1.2f;
// This is added to steerTime per m/s of velocity, so steering is slower when the car is moving faster.
public float veloSteerTime = 0.1f;
// How long it takes to fully turn the steering wheel from full lock to center
public float steerReleaseTime = 0.6f;
// This is added to steerReleaseTime per m/s of velocity, so steering is slower when the car is moving faster.
public float veloSteerReleaseTime = 0f;
// When detecting a situation where the player tries to counter steer to correct an oversteer situation,
// steering speed will be multiplied by the difference between optimal and current steering times this
// factor, to make the correction easier.
public float steerCorrectionFactor = 4.0f;
// Used by SoundController to get average slip velo of all wheels for skid sounds.
public float slipVelo {
get {
float val = 0.0f;
foreach(Wheel w in wheels)
val += w.slipVelo / wheels.Length;
return val;
}
}
// Initialize
void Start ()
{
if (centerOfMass != null)
rigidbody.centerOfMass = centerOfMass.localPosition;
rigidbody.inertiaTensor *= inertiaFactor;
drivetrain = GetComponent (typeof (Drivetrain)) as Drivetrain;
}
void Update ()
{
rigidbody.drag = rigidbody.velocity.magnitude / 1000;
// Steering
Vector3 carDir = transform.forward;
float fVelo = rigidbody.velocity.magnitude;
Vector3 veloDir = rigidbody.velocity * (1/fVelo);
float angle = -Mathf.Asin(Mathf.Clamp( Vector3.Cross(veloDir, carDir).y, -1, 1));
float optimalSteering = angle / (wheels[0].maxSteeringAngle * Mathf.Deg2Rad);
if (fVelo < 1)
optimalSteering = 0;
float steerInput = 0;
if (Input.GetKey (KeyCode.LeftArrow))
steerInput = -1;
if (Input.GetKey (KeyCode.RightArrow))
steerInput = 1;
if (steerInput < steering)
{
float steerSpeed = (steering>0)?(1/(steerReleaseTime+veloSteerReleaseTime*fVelo)) :(1/(steerTime+veloSteerTime*fVelo));
if (steering > optimalSteering)
steerSpeed *= 1 + (steering-optimalSteering) * steerCorrectionFactor;
steering -= steerSpeed * Time.deltaTime;
if (steerInput > steering)
steering = steerInput;
}
else if (steerInput > steering)
{
float steerSpeed = (steering<0)?(1/(steerReleaseTime+veloSteerReleaseTime*fVelo)) :(1/(steerTime+veloSteerTime*fVelo));
if (steering < optimalSteering)
steerSpeed *= 1 + (optimalSteering-steering) * steerCorrectionFactor;
steering += steerSpeed * Time.deltaTime;
if (steerInput < steering)
steering = steerInput;
}
// Throttle/Brake
bool accelKey = Input.GetKey (KeyCode.UpArrow);
bool brakeKey = Input.GetKey (KeyCode.DownArrow);
if (drivetrain.automatic && drivetrain.gear == 0)
{
accelKey = Input.GetKey (KeyCode.DownArrow);
brakeKey = Input.GetKey (KeyCode.UpArrow);
}
if (Input.GetKey (KeyCode.LeftShift))
{
throttle += Time.deltaTime / throttleTime;
throttleInput += Time.deltaTime / throttleTime;
}
else if (accelKey)
{
if (drivetrain.slipRatio < 0.10f)
throttle += Time.deltaTime / throttleTime;
else if (!tractionControl)
throttle += Time.deltaTime / throttleTimeTraction;
else
throttle -= Time.deltaTime / throttleReleaseTime;
if (throttleInput < 0)
throttleInput = 0;
throttleInput += Time.deltaTime / throttleTime;
brake = 0;
}
else
{
if (drivetrain.slipRatio < 0.2f)
throttle -= Time.deltaTime / throttleReleaseTime;
else
throttle -= Time.deltaTime / throttleReleaseTimeTraction;
}
throttle = Mathf.Clamp01 (throttle);
if (brakeKey)
{
if (drivetrain.slipRatio < 0.2f)
brake += Time.deltaTime / throttleTime;
else
brake += Time.deltaTime / throttleTimeTraction;
throttle = 0;
throttleInput -= Time.deltaTime / throttleTime;
}
else
{
if (drivetrain.slipRatio < 0.2f)
brake -= Time.deltaTime / throttleReleaseTime;
else
brake -= Time.deltaTime / throttleReleaseTimeTraction;
}
brake = Mathf.Clamp01 (brake);
throttleInput = Mathf.Clamp (throttleInput, -1, 1);
// Handbrake
handbrake = Mathf.Clamp01 ( handbrake + (Input.GetKey (KeyCode.Space)? Time.deltaTime: -Time.deltaTime) );
// Gear shifting
float shiftThrottleFactor = Mathf.Clamp01((Time.time - lastShiftTime)/shiftSpeed);
drivetrain.throttle = throttle * shiftThrottleFactor;
drivetrain.throttleInput = throttleInput;
if(Input.GetKeyDown(KeyCode.A))
{
lastShiftTime = Time.time;
drivetrain.ShiftUp ();
}
if(Input.GetKeyDown(KeyCode.Z))
{
lastShiftTime = Time.time;
drivetrain.ShiftDown ();
}
// Apply inputs
foreach(Wheel w in wheels)
{
w.brake = brake;
w.handbrake = handbrake;
w.steering = steering;
}
}
// Debug GUI. Disable when not needed.
void OnGUI ()
{
GUI.Box(new Rect(10, 40, 150, 20), "Скорость: " + Mathf.Abs(Mathf.Round(rigidbody.velocity.magnitude * 3.6f)) + " км/ч");
}
}
5) Противник – машина инопланетян. В одиночных миссиях инопланетяне выступают как враги медведей и управление ими берёт на себя искусственный интеллект гонки(далее ИИ). Однако для создания противника сначала так же была прорисована модель в 3dmax и далее были наложены colliders. Что мы можем видеть на скриншоте.

Теперь поговорим о скрипте управления, ИИ реализовывал алгоритм движения по точкам. На трассе были расставлены пустые объекты(waypoints), по которым происходило само ориентирование. При этом ИИ сам приостанавливал машину если точка находилась близко, тем самым машина хорошо входила в повороты и достаточно адекватно вела себя на трассе. Привожу листинг кода по управлению ИИ машиной:
// These variables allow the script to power the wheels of the car.
var FrontLeftWheel : WheelCollider;
var FrontRightWheel : WheelCollider;
// These variables are for the gears, the array is the list of ratios. The script
// uses the defined gear ratios to determine how much torque to apply to the wheels.
var GearRatio : float[];
var CurrentGear : int = 0;
// These variables are just for applying torque to the wheels and shifting gears.
// using the defined Max and Min Engine RPM, the script can determine what gear the
// car needs to be in.
var EngineTorque : float = 600.0;
var MaxEngineRPM : float = 3000.0;
var MinEngineRPM : float = 1000.0;
private var EngineRPM : float = 0.0;
// Here's all the variables for the AI, the waypoints are determined in the "GetWaypoints" function.
// the waypoint container is used to search for all the waypoints in the scene, and the current
// waypoint is used to determine which waypoint in the array the car is aiming for.
var waypointContainer : GameObject;
private var waypoints : Array;
private var currentWaypoint : int = 0;
// input steer and input torque are the values substituted out for the player input. The
// "NavigateTowardsWaypoint" function determines values to use for these variables to move the car
// in the desired direction.
private var inputSteer : float = 0.0;
private var inputTorque : float = 0.0;
function Start () {
// I usually alter the center of mass to make the car more stable. I'ts less likely to flip this way.
rigidbody.centerOfMass.y = -1.5;
// Call the function to determine the array of waypoints. This sets up the array of points by finding
// transform components inside of a source container.
GetWaypoints();
}
function Update () {
// This is to limith the maximum speed of the car, adjusting the drag probably isn't the best way of doing it,
// but it's easy, and it doesn't interfere with the physics processing.
rigidbody.drag = rigidbody.velocity.magnitude / 250;
// Call the funtion to determine the desired input values for the car. This essentially steers and
// applies gas to the engine.
NavigateTowardsWaypoint();
// Compute the engine RPM based on the average RPM of the two wheels, then call the shift gear function
EngineRPM = (FrontLeftWheel.rpm + FrontRightWheel.rpm)/2 * GearRatio[CurrentGear];
ShiftGears();
// set the audio pitch to the percentage of RPM to the maximum RPM plus one, this makes the sound play
// up to twice it's pitch, where it will suddenly drop when it switches gears.
audio.pitch = Mathf.Abs(EngineRPM / MaxEngineRPM) + 1.0 ;
// this line is just to ensure that the pitch does not reach a value higher than is desired.
if ( audio.pitch > 2.0 ) {
audio.pitch = 2.0;
}
// finally, apply the values to the wheels. The torque applied is divided by the current gear, and
// multiplied by the calculated AI input variable.
FrontLeftWheel.motorTorque = EngineTorque / GearRatio[CurrentGear] * inputTorque;
FrontRightWheel.motorTorque = EngineTorque / GearRatio[CurrentGear] * inputTorque;
// the steer angle is an arbitrary value multiplied by the calculated AI input.
FrontLeftWheel.steerAngle = 10 * inputSteer;
FrontRightWheel.steerAngle = 10 * inputSteer;
}
function ShiftGears() {
// this funciton shifts the gears of the vehcile, it loops through all the gears, checking which will make
// the engine RPM fall within the desired range. The gear is then set to this "appropriate" value.
if ( EngineRPM >= MaxEngineRPM ) {
var AppropriateGear : int = CurrentGear;
for ( var i = 0; i < GearRatio.length; i ++ ) {
if ( FrontLeftWheel.rpm * GearRatio[i] < MaxEngineRPM ) {
AppropriateGear = i;
break;
}
}
CurrentGear = AppropriateGear;
}
if ( EngineRPM <= MinEngineRPM ) {
AppropriateGear = CurrentGear;
for ( var j = GearRatio.length-1; j >= 0; j -- ) {
if ( FrontLeftWheel.rpm * GearRatio[j] > MinEngineRPM ) {
AppropriateGear = j;
break;
}
}
CurrentGear = AppropriateGear;
}
}
function GetWaypoints () {
// Now, this function basically takes the container object for the waypoints, then finds all of the transforms in it,
// once it has the transforms, it checks to make sure it's not the container, and adds them to the array of waypoints.
var potentialWaypoints : Array = waypointContainer.GetComponentsInChildren( Transform );
waypoints = new Array();
for ( var potentialWaypoint : Transform in potentialWaypoints ) {
if ( potentialWaypoint != waypointContainer.transform ) {
waypoints[ waypoints.length ] = potentialWaypoint;
}
}
}
function NavigateTowardsWaypoint () {
// now we just find the relative position of the waypoint from the car transform,
// that way we can determine how far to the left and right the waypoint is.
var RelativeWaypointPosition : Vector3 = transform.InverseTransformPoint( Vector3(
waypoints[currentWaypoint].position.x,
transform.position.y,
waypoints[currentWaypoint].position.z ) );
// by dividing the horizontal position by the magnitude, we get a decimal percentage of the turn angle that we can use to drive the wheels
inputSteer = RelativeWaypointPosition.x / RelativeWaypointPosition.magnitude;
// now we do the same for torque, but make sure that it doesn't apply any engine torque when going around a sharp turn...
if ( Mathf.Abs( inputSteer ) < 0.5 ) {
inputTorque = RelativeWaypointPosition.z / RelativeWaypointPosition.magnitude - Mathf.Abs( inputSteer );
}else{
inputTorque = 0.0;
}
// this just checks if the car's position is near enough to a waypoint to count as passing it, if it is, then change the target waypoint to the
// next in the list.
if ( RelativeWaypointPosition.magnitude < 20 ) {
currentWaypoint ++;
if ( currentWaypoint >= waypoints.length ) {
currentWaypoint = 0;
}
}
}
6) Стрельба. В игре так же была реализована возможность стрелять в противников и наносить им повреждения. Для этого были сделаны два скрипта, написанных на C#. Один из них отвечал за сам выстрел, другой за повреждение объекта от выстрела.
Листинг кода скрипта стрельбы:
using UnityEngine;
using System.Collections;
public class shoot : MonoBehaviour
{
public GameObject decalPrefab;
public Transform thePrefab;
public float forceSpeed = 100;
public float Range = 100;
public float shootTimer = 0;
public float shootCooler = 1;
public float damage = 20;
// Use this for initialization
void Fire ()
{
Vector3 DerectionRay = transform.TransformDirection (Vector3.forward);
RaycastHit Hit;
Transform prefabInstance = (Transform)Instantiate (thePrefab, GameObject.Find ("spawn_point").transform.position,
Quaternion.identity);
prefabInstance.rigidbody.AddForce (transform.forward * 10000);
prefabInstance.rigidbody.AddForce (transform.up * 1000);
if (Physics.Raycast (transform.position, DerectionRay, out Hit, Range)) {
if (Hit.rigidbody) {
Hit.rigidbody.AddForceAtPosition (DerectionRay * forceSpeed, Hit.point);
if (Hit.collider) {
Hit.collider.SendMessageUpwards ("ApplyDamage", damage, SendMessageOptions.DontRequireReceiver);
}
}
}
}
// Update is called once per framere
void Update ()
{
if (shootTimer > 0) {
shootTimer -= Time.deltaTime;
}
if (Input.GetKeyDown (KeyCode.Space) && shootTimer <= 0) {
Fire ();
shootTimer = shootCooler;
}
}
}
Листинг кода повреждения:
using UnityEngine;
using System.Collections;
public class AIDamage : MonoBehaviour
{
public int MaxHealth = 100;
public int currentHealth ;
public bool isPlayer = false;
//public Vector3 position;
public GameObject HealthBar;
// public GameObject HealthBack;
//private GameObject MyHealthBack;
private GameObject MyHealthBar;
private float healthW;
private float healthBarWidth;
// Use this for initialization
void Start ()
{
currentHealth = MaxHealth;
healthW = healthBarWidth = 100;
MyHealthBar = (GameObject)Instantiate (HealthBar, transform.position, transform.rotation);
// MyHealthBack = (GameObject)Instantiate (HealthBack, transform.position, transform.rotation);
}
void ApplyDamage (int Damage)
{
if (currentHealth != 0) {
currentHealth -= Damage;
}
float HealthPercent = (float)currentHealth / MaxHealth;
healthBarWidth = HealthPercent * healthW;
}
void OnGUI() {
if (isPlayer == false)
return;
GUI.Box(new Rect(10,10,healthBarWidth,20),"");
GUI.Box(new Rect(10,10,healthW,20), currentHealth + "/" + MaxHealth);
}
// Update is called once per frame
void Update ()
{
if (isPlayer == true)
return;
Vector3 position = Camera.main.WorldToViewportPoint (transform.position);
if (position.z < 0)
return;
position = new Vector3 (position.x - (float)0.05, position.y + (float)0.12, position.z);
MyHealthBar.transform.position = position;
//MyHealthBack.transform.position = position;
MyHealthBar.transform.localScale = Vector3.zero;
// MyHealthBack.transform.localScale = Vector3.zero;
MyHealthBar.guiTexture.pixelInset = new Rect (10, 10, healthBarWidth, 5);
// MyHealthBack.guiTexture.pixelInset = new Rect (6, 7, healthW + 8, 12);
if (currentHealth <= 0) {
Destroy (gameObject, (float)1);
}
}
