这是一款在unity环境中制作的小音游,参考《代码本色》教程,主要运用了书中力、粒子系统、物理函数库等章节的动画技术,实现了一个基于音乐旋律的交互应用。
玩家可自行添加歌曲(前提是电脑安装unity)并在开始界面中选择音乐,点击音乐按钮进入游戏,游戏主场景由五条弦及弦上的五个不同颜色的音符按钮组成,五个音符按钮从左到右依次由键盘上的数字键(非小键盘数字键)1、2、3、4、5控制,音乐开始后每条弦上的音符按钮会根据音乐旋律迎面移动过来,玩家需要根据音乐旋律及按钮提示及时按下相应音符按钮,按键正确时按钮上方会有火焰冒出,玩家得分增加,到音乐后半段有高音出现时还会在峰值间接出现带有尾巴的音符按钮,有助于玩家控制节奏。一首歌曲播放完毕后显示的菜单列出了音乐名称、玩家得分、玩家正确音符数、音乐总音符数及玩家正确率。
(因为只能上传gif所以没有声音。。大家领会精神吧【无奈脸)
开始部分:
高音部分:
结尾部分:
using UnityEngine;
using System.Collections;
using UnityEditor;
[CustomEditor( typeof( StringButton ) )]
public class StringButtonEditor : Editor
{
protected float LightIntensity = 0;
protected Color OldColor;
void Awake()
{
OnInspectorOpen();
}
void OnDisable()
{
OnInspectorClose();
}
protected void OnInspectorOpen()
{
EnableLightOfTargetObject( true );
LightIntensity = GetTargetsLightObject().GetComponent().intensity;
}
protected void OnInspectorClose()
{
EnableLightOfTargetObject( false );
}
public override void OnInspectorGUI()
{
DrawDefaultInspector();
DrawLightIntensitySlider();
if( HasChanged() )
{
OnInspectorChanged();
}
RememberColorToCheckForChange();
}
protected void OnInspectorChanged()
{
UpdateGuitarButtonColor();
UpdateLightIntensity();
UpdateKeyCodeLabel();
}
protected bool HasChanged()
{
if( GUI.changed )
{
return true;
}
if( GetTarget().Color != OldColor )
{
return true;
}
return false;
}
protected void RememberColorToCheckForChange()
{
OldColor = GetTarget().Color;
}
protected void DrawLightIntensitySlider()
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space( 15 );
GUILayout.Label( "Light Intensity", EditorStyles.structHeadingLabel );
LightIntensity = GUILayout.HorizontalSlider( LightIntensity, 0, 8 );
EditorGUILayout.EndHorizontal();
}
protected void UpdateGuitarButtonColor()
{
GameObject.Find( "Guitar" ).GetComponent().UpdateColorsArray();
}
protected void UpdateLightIntensity()
{
GetTargetsLightObject().GetComponent().intensity = LightIntensity;
}
protected void UpdateKeyCodeLabel()
{
KeyCode key = GetTarget().Key;
string keyString = key.ToString();
if( IsKeyAlphaNumeric( key ) )
{
keyString = keyString.Substring( 5, 1 ); //Strip String of "Alpha" pretext
}
SetTargetsKeyCodeLabel( keyString );
}
protected void SetTargetsKeyCodeLabel( string text )
{
GetTargetsGameObject().transform.Find( "KeyCode Label" ).GetComponent().text = text;
}
protected bool IsKeyAlphaNumeric( KeyCode key )
{
return key >= KeyCode.Alpha0 && key <= KeyCode.Alpha9;
}
protected void EnableLightOfTargetObject( bool enabled )
{
GetTargetsLightObject().GetComponent().enabled = enabled;
}
protected GameObject GetTargetsGameObject()
{
return GetTarget().gameObject;
}
protected GameObject GetTargetsLightObject()
{
return GetTargetsGameObject().transform.Find( "Light" ).gameObject;
}
protected StringButton GetTarget()
{
return ( target as StringButton );
}
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class GuitarGameplay : MonoBehaviour
{
public GameObject NotePrefab;
public SongData[] Playlist;
//References to important objects or components
protected GameObject GuitarNeckObject;
protected KeyboardControl KeyboardControl;
protected SongPlayer Player;
protected List NoteObjects;
protected Color[] Colors;
//Game state variables
protected float Score = 0f;
protected float Multiplier = 1f;
protected float Streak = 0;
protected float MaxStreak = 0;
protected float NumNotesHit = 0;
protected float NumNotesMissed = 0;
protected bool[] HasHitNoteOnStringIndexThisFrame;
//Use this for initialization
void Start()
{
//Init references to external objects/components
KeyboardControl = GameObject.Find( "Guitar" ).GetComponent();
GuitarNeckObject = transform.Find( "Guitar Neck" ).gameObject;
Player = GetComponent();
NoteObjects = new List();
HasHitNoteOnStringIndexThisFrame = new bool[ 5 ];
//Get string colors from buttons
UpdateColorsArray();
}
void Update()
{
if( Player.IsPlaying() )
{
//Check for ESC and possibly show in game menu
ShowInGameMenuOnKeypress();
//No note has been hit int this frame yet
ResetHasHitNoteOnStringIndexArray();
UpdateNeckTextureOffset();
UpdateNotes();
UpdateGuiScore();
UpdateGuiMultiplier();
}
}
public void UpdateColorsArray()
{
//If colors array is not initialized, do so
if( Colors == null || Colors.Length != 5 )
{
Colors = new Color[ 5 ];
}
//Get all five string buttons, get the colors from their StringButton component and apply it to the meshes and light
for( int i = 0; i < 5; ++i )
{
GameObject stringButton = GameObject.Find( "StringButton" + ( i + 1 ) );
Color color = stringButton.GetComponent().Color;
if( color != Colors[ i ] )
{
Colors[ i ] = color;
stringButton.transform.Find( "Paddle" ).GetComponent().sharedMaterial.color = color;
stringButton.transform.Find( "Socket" ).GetComponent().sharedMaterial.color = color;
stringButton.transform.Find( "Light" ).GetComponent().color = color;
}
}
}
public void StartPlaying( int playlistIndex )
{
ResetGameStateValues();
SetInGameUserInterfaceVisibility( true );
Player.SetSong( Playlist[ playlistIndex ] );
CreateNoteObjects();
Player.Play();
StartCoroutine( "DisplayCountdown" );
}
public void StopPlaying()
{
SetInGameUserInterfaceVisibility( false );
DestroyNoteObjects();
StopAllCoroutines();
}
protected void SetInGameUserInterfaceVisibility( bool show )
{
GameObject.Find( "GUI Score" ).GetComponent().enabled = show;
GameObject.Find( "GUI Multiplier" ).GetComponent().enabled = show;
}
protected IEnumerator StartAudio( float delay )
{
yield return new WaitForSeconds( delay );
Player.Play();
}
protected void CreateNoteObjects()
{
NoteObjects.Clear();
for( int i = 0; i < Player.Song.Notes.Count; ++i )
{
//Create note and trail objects
GameObject note = InstantiateNoteFromPrefab( Player.Song.Notes[ i ].StringIndex );
CreateTrailObject( note, Player.Song.Notes[ i ] );
//Hide object on start, they will be shown - when appropriate - in the UpdateNotes routine
note.GetComponent().enabled = false;
note.active = false;
NoteObjects.Add( note );
}
}
protected void DestroyNoteObjects()
{
for( int i = 0; i < NoteObjects.Count; ++i )
{
Destroy( NoteObjects[ i ] );
}
NoteObjects.Clear();
}
public SongData[] GetPlaylist()
{
return Playlist;
}
protected IEnumerator DisplayCountdown()
{
//Count down from 4 to 1 and GO at the beginning of a song
yield return new WaitForSeconds( MyMath.BeatsToSeconds( 0.5f, Player.Song.BeatsPerMinute ) );
StartCoroutine( DisplayText( "4", 1f, 0f ) );
yield return new WaitForSeconds( MyMath.BeatsToSeconds( 1f, Player.Song.BeatsPerMinute ) );
StartCoroutine( DisplayText( "3", 1f, 0f ) );
yield return new WaitForSeconds( MyMath.BeatsToSeconds( 1f, Player.Song.BeatsPerMinute ) );
StartCoroutine( DisplayText( "2", 1f, 0f ) );
yield return new WaitForSeconds( MyMath.BeatsToSeconds( 1f, Player.Song.BeatsPerMinute ) );
StartCoroutine( DisplayText( "1", 1f, 0f ) );
yield return new WaitForSeconds( MyMath.BeatsToSeconds( 1f, Player.Song.BeatsPerMinute ) );
StartCoroutine( DisplayText( "Go", MyMath.BeatsToSeconds( 1.5f, Player.Song.BeatsPerMinute ), MyMath.BeatsToSeconds( 1f, Player.Song.BeatsPerMinute ) ) );
}
protected void StopCountdown()
{
StopCoroutine( "DisplayCountdown" );
GameObject.Find( "GUI Text" ).GetComponent().text = "";
}
protected void ResetHasHitNoteOnStringIndexArray()
{
for( int i = 0; i < 5; ++i )
{
HasHitNoteOnStringIndexThisFrame[ i ] = false;
}
}
protected void ShowInGameMenuOnKeypress()
{
if( Input.GetKeyDown( KeyCode.Escape ) )
{
StopCountdown();
GetComponent().ShowMenu();
}
}
protected void UpdateNotes()
{
for( int i = 0; i < NoteObjects.Count; ++i )
{
UpdateNotePosition( i );
if( IsNoteHit( i ) )
{
HideNote( i );
Score += 10f * Multiplier;
Streak++;
NumNotesHit++;
if( Streak > MaxStreak )
{
MaxStreak = Streak;
}
//Check if there is a trail
if( Player.Song.Notes[ i ].Length > 0f )
{
//Handle the trail
StartTrailHitRoutineForNote( i );
}
else
{
//No trail, just show the fire particles
ShowFireParticlesForNote( i );
}
}
if( WasNoteMissed( i ) )
{
HideNote( i );
Streak = 0;
Multiplier = 1;
NumNotesMissed++;
}
}
}
protected void StartTrailHitRoutineForNote( int index )
{
GameObject trail = NoteObjects[ index ].transform.Find( "Trail" ).gameObject;
StartCoroutine( TrailHitRoutine( index, trail ) );
}
protected void ShowFireParticlesForNote( int index )
{
StartCoroutine( ShowFireParticles( Player.Song.Notes[ index ].StringIndex ) );
}
protected IEnumerator ShowFireParticles( int stringIndex )
{
KeyboardControl.GetStringButton( stringIndex ).transform.Find( "Flame" ).GetComponent().ClearParticles();
KeyboardControl.GetStringButton( stringIndex ).transform.Find( "Flame" ).GetComponent().emit = true;
//Wait for one frame and disable the emitter again,
//it is set to one shot so everything is emitted on the first frame
yield return null;
KeyboardControl.GetStringButton( stringIndex ).transform.Find( "Flame" ).GetComponent().emit = false;
}
protected IEnumerator TrailHitRoutine( int noteIndex, GameObject trail )
{
Note note = Player.Song.Notes[ noteIndex ];
//Update the color of the trail
//Initially it is darkened and now the full bright color is applied
trail.GetComponent().material.color = Colors[ note.StringIndex ];
//Start the spark particles
KeyboardControl.GetStringButton( note.StringIndex ).transform.Find( "Sparks" ).GetComponent().emit = true;
KeyboardControl.GetStringButton( note.StringIndex ).transform.Find( "Sparks" ).GetComponent().GetComponent().enabled = true;
Vector3 trailScale = trail.transform.localScale;
Vector3 trailPosition = trail.transform.localPosition;
//Do this as long as the button for the specific string is pressed or until the trail reaches its end
while( KeyboardControl.IsButtonPressed( note.StringIndex )
&& Player.GetCurrentBeat() + 1 <= note.Time + note.Length )
{
//Calculate how far we have progressed in this trail
float progress = Mathf.Clamp01( ( 1 + Player.GetCurrentBeat() - note.Time ) / note.Length );
//Shrink the trail and adjust its position
//Since the pivot of the trail is in the center, meaning it will shrink at the start and at the end,
//we have to reposition the trail each frame so it appears as if its shrinking from the beginning and the
//end remains fixed
trail.transform.localScale = new Vector3( trailScale.x, trailScale.y, trailScale.z * ( 1 - progress ) );
trail.transform.localPosition = new Vector3( trailPosition.x, trailPosition.y + trailPosition.y * progress, trailPosition.z );
//Its possible to hit the note before its beat is reached, because the hit zone is wide to make it easier to hit the notes
//Increate the score only after the notes real hit position is reached
if( progress > 0 )
{
Score += 10 * Multiplier * Time.deltaTime;
}
yield return null;
}
//Hide the trail
trail.GetComponent().enabled = false;
//Disable the particles
KeyboardControl.GetStringButton( note.StringIndex ).transform.Find( "Sparks" ).GetComponent().emit = false;
KeyboardControl.GetStringButton( note.StringIndex ).transform.Find( "Sparks" ).GetComponent().GetComponent().enabled = false;
}
protected void HideNote( int index )
{
NoteObjects[ index ].GetComponent().enabled = false;
}
protected bool IsNoteHit( int index )
{
Note note = Player.Song.Notes[ index ];
//If no button is pressed on this notes string, it cannot be hit
if( !KeyboardControl.WasButtonJustPressed( note.StringIndex ) )
{
return false;
}
//If a note was already hit on this string during this frame, dont hit this one aswell
if( HasHitNoteOnStringIndexThisFrame[ note.StringIndex ] )
{
return false;
}
//When the renderer is disabled, this note was already hit before
if( NoteObjects[ index ].GetComponent().enabled == false )
{
return false;
}
//Check if this note is in the hit zone
if( IsInHitZone( NoteObjects[ index ] ) )
{
//Set this flag so no two notes are hit with the same button press
HasHitNoteOnStringIndexThisFrame[ note.StringIndex ] = true;
return true;
}
//The note is not in the hit zone, therefore cannot be hit
return false;
}
protected bool WasNoteMissed( int index )
{
//If position.z is greater than 0, this note can still be hit
if( NoteObjects[ index ].transform.position.z > 0 )
{
return false;
}
//If the renderer is disabled, this note was hit
if( NoteObjects[ index ].GetComponent().enabled == false )
{
return false;
}
//Yea, this note was missed
return true;
}
protected void ResetGameStateValues()
{
Score = 0;
Streak = 0;
MaxStreak = 0;
Multiplier = 1;
NumNotesMissed = 0;
NumNotesHit = 0;
}
protected void UpdateNotePosition( int index )
{
Note note = Player.Song.Notes[ index ];
//If the note is farther away then 6 beats, its not visible on the neck and we dont have to update it
if( note.Time < Player.GetCurrentBeat() + 6 )
{
//If the note is not active, it is visible on the neck for the first time
if( !NoteObjects[ index ].active )
{
//Activate and show the note
NoteObjects[ index ].active = true;
NoteObjects[ index ].GetComponent().enabled = true;
//If there is a trail, show that aswell
if( Player.Song.Notes[ index ].Length > 0f )
{
NoteObjects[ index ].transform.Find( "Trail" ).GetComponent().enabled = true;
}
}
//Calculate how far the note has progressed on the neck
float progress = ( note.Time - Player.GetCurrentBeat() - 0.5f ) / 6f;
//Update its position
Vector3 position = NoteObjects[ index ].transform.position;
position.z = progress * GetGuitarNeckLength();
NoteObjects[ index ].transform.position = position;
}
}
protected void UpdateGuiScore()
{
GameObject.Find( "GUI Score" ).GetComponent().text = Mathf.Floor( Score ).ToString();
}
protected void UpdateGuiMultiplier()
{
Multiplier = Mathf.Ceil( Streak / 10 );
Multiplier = Mathf.Clamp( Multiplier, 1, 10 );
GameObject.Find( "GUI Multiplier" ).GetComponent().text = "x" + Mathf.Floor( Multiplier ).ToString();
}
protected void UpdateNeckTextureOffset()
{
//Get the current offset
Vector2 offset = GuitarNeckObject.GetComponent().material.GetTextureOffset( "_MainTex" );
//Update its y component
offset.y = 1 - ( Player.GetCurrentBeat() - 0.5f ) / 6f;
//And set it again
GuitarNeckObject.GetComponent().material.SetTextureOffset( "_MainTex", offset );
}
protected float GetNeckMoveOffset()
{
return Time.deltaTime * Player.Song.BeatsPerMinute * ( 1f / 6f / 60f );
}
protected bool IsInHitZone( GameObject note )
{
return note.transform.position.z < GetHitZoneBeginning()
&& note.transform.position.z > GetHitZoneEnd();
}
protected float GetGuitarNeckLength()
{
return 20f;
}
public Color GetColor( int index )
{
if( Colors == null || Colors.Length != 5 )
{
UpdateColorsArray();
}
return Colors[ index ];
}
public float GetScore()
{
return Score;
}
public float GetMaximumStreak()
{
return MaxStreak;
}
public float GetNumNotesHit()
{
return NumNotesHit;
}
public float GetNumNotesMissed()
{
return NumNotesMissed;
}
protected Vector3 GetStartPosition( int stringIndex )
{
return new Vector3( (float)( stringIndex - 2 ), 0f, GetGuitarNeckLength() );
}
protected GameObject InstantiateNoteFromPrefab( int stringIndex )
{
GameObject note = Instantiate( NotePrefab
, GetStartPosition( stringIndex )
, Quaternion.identity
) as GameObject;
note.GetComponent().material.color = Colors[ stringIndex ];
note.transform.Rotate( new Vector3( -90, 0, 0 ) );
return note;
}
protected GameObject CreateTrailObject( GameObject noteObject, Note note )
{
if( note.Length == 0 )
{
return null;
}
GameObject trail = GameObject.CreatePrimitive( PrimitiveType.Plane );
//We don't need the collider of the plane
Destroy( trail.GetComponent() );
//The guitar neck is 20 units long
//The plane primitive is 10 units long
//1/6th of the neck is one beat, therefore 1/3rd of the initial plane length
float scaleZ = 0.33f * note.Length;
//Scale the plane
trail.transform.localScale = new Vector3( 0.03f, 1f, scaleZ );
//Add the trail as child of the note
trail.transform.parent = noteObject.transform;
//position it so that the trail is behind the note
trail.transform.localPosition = new Vector3( 0f, -10f * scaleZ / 2f, 0.01f );
//Setup colors and shader
trail.GetComponent().material.color = Colors[ note.StringIndex ] * 0.2f;
trail.GetComponent().material.shader = Shader.Find( "Self-Illumin/Diffuse" );
trail.GetComponent().enabled = false;
trail.name = "Trail";
return trail;
}
protected float GetHitZoneBeginning()
{
//Adjust this if you want to enable the player to hit the note earlier
return 2.4f;
}
protected float GetHitZoneEnd()
{
//Adjust this if you want to enable the player to hit the note later
return 0.95f;
}
public void OnSongFinished()
{
GameObject.Find( "GUI Score" ).GetComponent().enabled = false;
GameObject.Find( "GUI Multiplier" ).GetComponent().enabled = false;
GetComponent().ShowMenu();
}
protected IEnumerator DisplayText( string text, float duration, float fade = 0f )
{
//Display a Text in the center of the screen for 'duration' seconds
GameObject guiTextObject = GameObject.Find( "GUI Text" );
guiTextObject.GetComponent().text = text;
//Make text visible
Color newColor = guiTextObject.GetComponent().material.color;
newColor.a = 1;
guiTextObject.GetComponent().material.color = newColor;
//Wait for duration - fade
yield return new WaitForSeconds( MyMath.BeatsToSeconds( duration - fade, Player.Song.BeatsPerMinute ) );
//Fade out text
float fadeTime = MyMath.BeatsToSeconds( fade, Player.Song.BeatsPerMinute );
float totalFadeTime = fadeTime;
while( fadeTime > 0 && guiTextObject.GetComponent().text == text )
{
newColor = guiTextObject.GetComponent().material.color;
newColor.a = fadeTime / totalFadeTime;
guiTextObject.GetComponent().material.color = newColor;
fadeTime -= Time.deltaTime;
yield return null;
}
//If no other DisplayText() Coroutine has changed the text, hide it
if( guiTextObject.GetComponent().text == text )
{
guiTextObject.GetComponent().text = "";
}
}
}
using UnityEngine;
using System.Collections;
public class KeyboardControl : MonoBehaviour
{
//This constant is only used in this class and cannot be adjusted to work with more or less strings
//This feature is planned for the future
const int NumStrings = 5;
//The five button objects in the scene
protected GameObject[] StringButtons;
//Stores if the button is held down
protected bool[] ButtonsPressed;
//Stores if the button was just pressed in this frame
protected bool[] ButtonsJustPressed;
//KeyCodes that control the five string buttons
protected KeyCode[] StringKeys;
//Use this for initialization
void Start()
{
ButtonsPressed = new bool[ NumStrings ];
ButtonsJustPressed = new bool[ NumStrings ];
for( int i = 0; i < NumStrings; ++i )
{
ButtonsPressed[ i ] = false;
ButtonsJustPressed[ i ] = false;
}
UpdateStringKeyArray();
SaveReferencesToStringButtons();
}
protected void UpdateStringKeyArray()
{
StringKeys = new KeyCode[ NumStrings ];
for( int i = 0; i < NumStrings; ++i )
{
StringKeys[ i ] = GameObject.Find( "StringButton" + ( i + 1 ) ).GetComponent().Key;
}
}
void SaveReferencesToStringButtons()
{
StringButtons = new GameObject[ NumStrings ];
for( int i = 0; i < NumStrings; ++i )
{
StringButtons[ i ] = GameObject.Find( "StringButton" + ( i + 1 ) );
}
}
//Update is called once per frame
void Update()
{
ProcessKeyInput();
}
void ProcessKeyInput()
{
ResetButtonsJustPressedArray();
for( int i = 0; i < NumStrings; ++i )
{
CheckKeyCode( StringKeys[ i ], i );
}
}
void CheckKeyCode( KeyCode code, int stringIndex )
{
if( Input.GetKeyDown( code ) )
{
OnStringChange( stringIndex, true );
}
if( Input.GetKeyUp( code ) )
{
OnStringChange( stringIndex, false );
}
if( Input.GetKey( code ) && !ButtonsPressed[ stringIndex ] )
{
OnStringChange( stringIndex, true );
}
}
protected void ResetButtonsJustPressedArray()
{
for( int i = 0; i < NumStrings; ++i )
{
ButtonsJustPressed[ i ] = false;
}
}
protected int GetNumButtonsPressed()
{
int pressed = 0;
for( int i = 0; i < NumStrings; ++i )
{
if( ButtonsPressed[ i ] )
{
pressed++;
}
}
return pressed;
}
public GameObject GetStringButton( int index )
{
return StringButtons[ index ];
}
public bool IsButtonPressed( int index )
{
return ButtonsPressed[ index ];
}
public bool WasButtonJustPressed( int index )
{
return ButtonsJustPressed[ index ];
}
void OnStringChange( int stringIndex, bool pressed )
{
Vector3 stringButtonPosition = StringButtons[ stringIndex ].transform.Find( "Paddle" ).transform.position;
if( pressed )
{
//Only press this if less then two buttons are already pressed
//The keyboard limits multiple key presses arbitrarily, sometimes its 2, sometimes 3
//So I locked it to a maximum of two key presses at the same time for consistency
if( GetNumButtonsPressed() < 2 )
{
//Move the paddle upwards
stringButtonPosition.y = 0.16f;
//Enable the light
StringButtons[ stringIndex ].transform.Find( "Light" ).GetComponent().enabled = true;
//Update key state
ButtonsPressed[ stringIndex ] = true;
ButtonsJustPressed[ stringIndex ] = true;
}
}
else
{
//Move paddle down
stringButtonPosition.y = -0.06f;
//Disable light
StringButtons[ stringIndex ].transform.Find( "Light" ).GetComponent().enabled = false;
//Update key state
ButtonsPressed[ stringIndex ] = false;
}
//Set paddle position
StringButtons[ stringIndex ].transform.Find( "Paddle" ).transform.position = stringButtonPosition;
}
}