I'm trying to make a Gear VR application with the Gear VR controller and the Oculus SDK.
I got the GazePointerRing to work with the controller prefab. There is a reticle in my app visible that i can move around with the Gear VR controller. It detects the cubes that I've placed in the scene.
What I want to do now is point the reticle at a cube and hold a button on the controller, so that the cube will stick to my controller model and can move around until I let go of the button. I've been searching in the OVR Physics Raycaster script how i can call upon the raycast hit and input it in a button command in an if statement. But i can't find a way to call upon the raycast hit with an object.
This is the Oculus code in the OVR Physics Raycaster script:
using System.Collections.Generic;
namespace UnityEngine.EventSystems
{
/// <summary>
/// Simple event system using physics raycasts.
/// </summary>
[RequireComponent(typeof(OVRCameraRig))]
public class OVRPhysicsRaycaster : BaseRaycaster
{
/// <summary>
/// Const to use for clarity when no event mask is set
/// </summary>
protected const int kNoEventMaskSet = -1;
/// <summary>
/// Layer mask used to filter events. Always combined with the camera's culling mask if a camera is used.
/// </summary>
[SerializeField]
public LayerMask m_EventMask = kNoEventMaskSet;
protected OVRPhysicsRaycaster()
{ }
public override Camera eventCamera
{
get
{
return GetComponent<OVRCameraRig>().leftEyeCamera;
}
}
/// <summary>
/// Depth used to determine the order of event processing.
/// </summary>
public virtual int depth
{
get { return (eventCamera != null) ? (int)eventCamera.depth : 0xFFFFFF; }
}
/// <summary>
/// Event mask used to determine which objects will receive events.
/// </summary>
public int finalEventMask
{
get { return (eventCamera != null) ? eventCamera.cullingMask & m_EventMask : kNoEventMaskSet; }
}
/// <summary>
/// Layer mask used to filter events. Always combined with the camera's culling mask if a camera is used.
/// </summary>
public LayerMask eventMask
{
get { return m_EventMask; }
set { m_EventMask = value; }
}
/// <summary>
/// Perform a raycast using the worldSpaceRay in eventData.
/// </summary>
/// <param name="eventData"></param>
/// <param name="resultAppendList"></param>
public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
{
// This function is closely based on PhysicsRaycaster.Raycast
if (eventCamera == null)
return;
OVRRayPointerEventData rayPointerEventData = eventData as OVRRayPointerEventData;
if (rayPointerEventData == null)
return;
var ray = rayPointerEventData.worldSpaceRay;
float dist = eventCamera.farClipPlane - eventCamera.nearClipPlane;
var hits = Physics.RaycastAll(ray, dist, finalEventMask);
if (hits.Length > 1)
System.Array.Sort(hits, (r1, r2) => r1.distance.CompareTo(r2.distance));
if (hits.Length != 0)
{
for (int b = 0, bmax = hits.Length; b < bmax; ++b)
{
var result = new RaycastResult
{
gameObject = hits[b].collider.gameObject,
module = this,
distance = hits[b].distance,
index = resultAppendList.Count,
worldPosition = hits[0].point,
worldNormal = hits[0].normal,
};
resultAppendList.Add(result);
}
}
}
/// <summary>
/// Perform a Spherecast using the worldSpaceRay in eventData.
/// </summary>
/// <param name="eventData"></param>
/// <param name="resultAppendList"></param>
/// <param name="radius">Radius of the sphere</param>
public void Spherecast(PointerEventData eventData, List<RaycastResult> resultAppendList, float radius)
{
if (eventCamera == null)
return;
OVRRayPointerEventData rayPointerEventData = eventData as OVRRayPointerEventData;
if (rayPointerEventData == null)
return;
var ray = rayPointerEventData.worldSpaceRay;
float dist = eventCamera.farClipPlane - eventCamera.nearClipPlane;
var hits = Physics.SphereCastAll(ray, radius, dist, finalEventMask);
if (hits.Length > 1)
System.Array.Sort(hits, (r1, r2) => r1.distance.CompareTo(r2.distance));
if (hits.Length != 0)
{
for (int b = 0, bmax = hits.Length; b < bmax; ++b)
{
var result = new RaycastResult
{
gameObject = hits[b].collider.gameObject,
module = this,
distance = hits[b].distance,
index = resultAppendList.Count,
worldPosition = hits[0].point,
worldNormal = hits[0].normal,
};
resultAppendList.Add(result);
}
}
}
/// <summary>
/// Get screen position of this world position as seen by the event camera of this OVRPhysicsRaycaster
/// </summary>
/// <param name="worldPosition"></param>
/// <returns></returns>
public Vector2 GetScreenPos(Vector3 worldPosition)
{
// In future versions of Uinty RaycastResult will contain screenPosition so this will not be necessary
return eventCamera.WorldToScreenPoint(worldPosition);
}
}
}
prerequisite: Ensure to have OVR Manager in your scene, it is a singleton and required for the GearVR controller (OVRInput class) to work.
My usual approach is to raycast a ray from the controller anchor position going forward and check if it hits a desired object
public class SampleScript : MonoBehaviour
{
public Transform anchorPos;
public GameObject detectionLineObject; // a gameObject with a line renderer
private RaycastHit _hitInfo;
private LineRenderer _detectionLine;
void Start()
{
GameObject line = Instantiate(detectionLineObject);
_detectionLine = line.GetComponent<LineRenderer>();
}
void Update()
{
DetectionManager();
}
void DetectionManager()
{
// check if controller is actually connected
if (!OVRInput.IsControllerConnected(OVRInput.Controller.RTrackedRemote) || !OVRInput.IsControllerConnected(OVRInput.Controller.LTrackedRemote))
{
return;
}
// launch a ray from the OVRCameraRig's Anchor Right
if (Physics.Raycast(anchorPos.position, anchorPos.forward, out _hitInfo))
{
// set our line points
_detectionLine.SetPosition(0, anchorPos.position);
_detectionLine.SetPosition(1, _hitInfo.point);
MyComponent target = _hitInfo.collider.gameObject.GetComponent<MyComponent>();
if (target != null)
{
// Do you stuff here
target.StartInteraction();
}
}
else
{
// point our line to infinity
_detectionLine.SetPosition(0, anchorPos.position);
_detectionLine.SetPosition(1, anchorPos.forward * 500.0f);
}
}
}
Related
I'm taking a coursera course where an assignment I'm working on involves completing work on a "take the last rock" style puzzle. For this assignment, the puzzle needs to run with two AI players 600 times at varying levels of AI difficulty. The problem is that after implementing my changes (which involve recursive Minimax algorithms) I'm running into an issue where the unity editor seems to freeze after a few games. It always seems to happen while the newgamedelaytimer is running (timer code provided by the instructor). I've watched it run in debug mode and after a few games the timer stops incrementing and I have to restart Unity.
Here is the code for the base game (where the newgamedelaytimer is called) and the course provided timer.
Is there something I'm missing here?
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.SceneManagement;
/// <summary>
/// Game manager
/// </summary>
public class DontTakeTheLastTeddy : MonoBehaviour, ITakeTurnInvoker, IGameOverInvoker,
IGameStartingInvoker
{
Board board;
Player player1;
Player player2;
// multiple games support
Timer newGameDelayTimer;
PlayerName firstMovePlayer = PlayerName.Player1;
const int TotalGames = 600;
int gamesPlayed = 0;
// events invoked by class
TakeTurn takeTurnEvent = new TakeTurn();
GameOver gameOverEvent = new GameOver();
GameStarting gameStartingEvent = new GameStarting();
#region Constructor
// Uncomment the code below after copying this class into the console
// app for the automated grader. DON'T uncomment it now; it won't
// compile in a Unity project
/// <summary>
/// Constructor
///
/// Note: The class in the Unity solution doesn't use a constructor;
/// this constructor is to support automated grading
/// </summary>
/// <param name="gameObject">the game object the script is attached to</param>
//public DontTakeTheLastTeddy(GameObject gameObject) :
// base(gameObject)
//{
//}
#endregion
/// <summary>
/// Awake is called before Start
///
/// Leave this method public to support automated grading
/// </summary>
public void Awake()
{
// retrieve board and player references
board = GameObject.FindGameObjectWithTag("Board").GetComponent<Board>();
player1 = GameObject.FindGameObjectWithTag("Player1").GetComponent<Player>();
player2 = GameObject.FindGameObjectWithTag("Player2").GetComponent<Player>();
// register as invoker and listener
EventManager.AddTakeTurnInvoker(this);
EventManager.AddGameOverInvoker(this);
EventManager.AddGameStartingInvoker(this);
EventManager.AddTurnOverListener(HandleTurnOverEvent);
EventManager.AddGameOverListener(HandleGameOverEvent);
// set up timer for delay between games
newGameDelayTimer = gameObject.AddComponent<Timer>();
newGameDelayTimer.Duration = 1.5f;
newGameDelayTimer.AddTimerFinishedListener(HandleTimerFinishedEvent);
// initialize statistics class
Statistics.Initialize();
}
private void HandleGameOverEvent(PlayerName arg0, Difficulty arg1, Difficulty arg2)
{
Debug.Log("Starting New Game Delay timer");
newGameDelayTimer.Run();
//HandleTimerFinishedEvent();
}
/// <summary>
/// Use this for initialization
/// </summary>
void Start()
{
StartGame(PlayerName.Player1, Difficulty.Easy, Difficulty.Easy);
}
/// <summary>
/// Adds the given listener for the TakeTurn event
/// </summary>
/// <param name="listener">listener</param>
public void AddTakeTurnListener(UnityAction<PlayerName, Configuration> listener)
{
takeTurnEvent.AddListener(listener);
}
/// <summary>
/// Adds the given listener for the GameOver event
/// </summary>
/// <param name="listener">listener</param>
public void AddGameOverListener(
UnityAction<PlayerName, Difficulty, Difficulty> listener)
{
gameOverEvent.AddListener(listener);
}
/// <summary>
/// Adds the given listener for the GameStarting event
/// </summary>
/// <param name="listener">listener</param>
public void AddGameStartingListener(UnityAction listener)
{
gameStartingEvent.AddListener(listener);
}
/// <summary>
/// Starts a game with the given player taking the
/// first turn
///
/// Leave this method public to support automated grading
/// </summary>
/// <param name="firstPlayer">player taking first turn</param>
/// <param name="player1Difficulty">difficulty for player 1</param>
/// <param name="player2Difficulty">difficulty for player 2</param>
public void StartGame(PlayerName firstPlayer, Difficulty player1Difficulty,
Difficulty player2Difficulty)
{
// set player difficulties
player1.Difficulty = player1Difficulty;
player2.Difficulty = player2Difficulty;
// create new board
board.CreateNewBoard();
takeTurnEvent.Invoke(firstPlayer,
board.Configuration);
}
/// <summary>
/// Handles the TurnOver event by having the
/// other player take their turn
/// </summary>
/// <param name="player">who finished their turn</param>
/// <param name="newConfiguration">the new board configuration</param>
void HandleTurnOverEvent(PlayerName player,
Configuration newConfiguration)
{
board.Configuration = newConfiguration;
// check for game over
if (newConfiguration.Empty)
{
// fire event with winner
if (player == PlayerName.Player1)
{
gameOverEvent.Invoke(PlayerName.Player2, player1.Difficulty, player2.Difficulty);
}
else
{
gameOverEvent.Invoke(PlayerName.Player1, player1.Difficulty, player2.Difficulty);
}
}
else
{
// game not over, so give other player a turn
if (player == PlayerName.Player1)
{
takeTurnEvent.Invoke(PlayerName.Player2,
newConfiguration);
}
else
{
takeTurnEvent.Invoke(PlayerName.Player1,
newConfiguration);
}
}
}
/// <summary>
/// Starts a new game when the new game delay timer finishes
/// </summary>
void HandleTimerFinishedEvent()
{
Debug.Log("Finished new Game Timer");
// constant provided for autograder support
if (!GameConstants.PlaySingleGame)
{
if (gamesPlayed < TotalGames)
{
if (gamesPlayed % 100 == 0)
{
// uncomment the line below and implement a
// SetPlayerDifficulties method
SetPlayerDifficulties(gamesPlayed);
}
gamesPlayed++;
//Debug.Log("Games Played: " + gamesPlayed);
// alternate player making first move in game
if (firstMovePlayer == PlayerName.Player1)
{
firstMovePlayer = PlayerName.Player2;
}
else
{
firstMovePlayer = PlayerName.Player1;
}
StartGame(firstMovePlayer, player1.Difficulty, player2.Difficulty);
}
else
{
// move to statistics scene when all games have been played
SceneManager.LoadScene("statistics");
}
}
}
private void SetPlayerDifficulties(int gamesPlayed)
{
if (gamesPlayed == 100)
{
player1.Difficulty = Difficulty.Medium;
player2.Difficulty = Difficulty.Medium;
}
else if (gamesPlayed == 200)
{
player1.Difficulty = Difficulty.Hard;
player2.Difficulty = Difficulty.Hard;
}
else if (gamesPlayed == 300)
{
player1.Difficulty = Difficulty.Easy;
player2.Difficulty = Difficulty.Medium;
}
else if (gamesPlayed == 400)
{
player2.Difficulty = Difficulty.Hard;
}
else if (gamesPlayed == 500)
{
player1.Difficulty = Difficulty.Medium;
player2.Difficulty = Difficulty.Hard;
}
}
}
Timer.cs
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// A timer
/// </summary>
public class Timer : MonoBehaviour
{
#region Fields
// timer duration
float totalSeconds = 0;
// timer execution
float elapsedSeconds = 0;
bool running = false;
// support for TimerFinished event
bool started = false;
TimerFinished timerFinished = new TimerFinished();
#endregion
#region Properties
/// <summary>
/// Sets the duration of the timer
/// The duration can only be set if the timer isn't currently running
/// </summary>
/// <value>duration</value>
public float Duration
{
set
{
if (!running)
{
totalSeconds = value;
}
}
}
/// <summary>
/// Gets whether or not the timer has finished running
/// This property returns false if the timer has never been started
/// </summary>
/// <value>true if finished; otherwise, false.</value>
public bool Finished
{
get { return started && !running; }
}
/// <summary>
/// Gets whether or not the timer is currently running
/// </summary>
/// <value>true if running; otherwise, false.</value>
public bool Running
{
get { return running; }
}
/// <summary>
/// Gets ho wmany seconds are left on the timer
/// </summary>
/// <value>seconds left</value>
public float SecondsLeft
{
get
{
if (running)
{
return totalSeconds - elapsedSeconds;
}
else
{
return 0;
}
}
}
#endregion
#region Methods
/// <summary>
/// Update is called once per frame
/// </summary>
void Update()
{
// update timer and check for finished
if (running)
{
elapsedSeconds += Time.fixedDeltaTime;
if (elapsedSeconds >= totalSeconds)
{
running = false;
timerFinished.Invoke();
}
}
}
/// <summary>
/// Runs the timer
/// Because a timer of 0 duration doesn't really make sense,
/// the timer only runs if the total seconds is larger than 0
/// This also makes sure the consumer of the class has actually
/// set the duration to something higher than 0
/// </summary>
public void Run()
{
// only run with valid duration
if (totalSeconds > 0)
{
started = true;
running = true;
elapsedSeconds = 0;
}
}
/// <summary>
/// Stops the timer
/// </summary>
public void Stop()
{
started = false;
running = false;
}
/// <summary>
/// Adds the given number of seconds to the timer
/// </summary>
/// <param name="seconds">time to add</param>
public void AddTime(float seconds)
{
totalSeconds += seconds;
}
/// <summary>
/// Adds the given listener for the TimerFinished event
/// </summary>
/// <param name="listener">listener</param>
public void AddTimerFinishedListener(UnityAction listener)
{
timerFinished.AddListener(listener);
}
#endregion
}
When playing sound, I need to adjust the volume of the right and left channels separately.
I have the class to play sound:
public class SoundPlayer
{
private WaveOutEvent _outputDevice;
private AudioFileReader _audioFile;
private float _volume = 1f;
public float Volume
{
get => _volume;
set
{
_volume = value;
if (_audioFile != null)
_audioFile.Volume = value;
}
}
public void Play(string fileName)
{
if (_outputDevice == null)
{
_outputDevice = new WaveOutEvent();
_outputDevice.PlaybackStopped += (sender, args) =>
{
_outputDevice.Dispose();
_outputDevice = null;
_audioFile.Dispose();
_audioFile = null;
};
}
if (_audioFile == null)
{
_audioFile = new AudioFileReader(fileName) { Volume = _volume };
_outputDevice.Init(_audioFile);
}
else
{
if (string.IsNullOrWhiteSpace(fileName))
_outputDevice = null;
else
{
if (_audioFile.FileName != fileName)
{
_audioFile = new AudioFileReader(fileName) { Volume = _volume };
_outputDevice.Init(_audioFile);
}
}
}
_outputDevice?.Play();
}
public void Stop()
{
_outputDevice?.Stop();
}
}
But in this class you can only adjust the overall volume. How to make such a property:
soundPlayer.LeftChannelVolume = 1.0f
soundPlayer.RightChannelVolume = 0.5f
I made my provider and it worked!)
If anyone needs it, then here:
/// <summary>
/// Very simple sample provider supporting adjustable gain
/// </summary>
public class VolumeStereoSampleProvider : ISampleProvider
{
private readonly ISampleProvider source;
/// <summary>
/// Allows adjusting the volume left channel, 1.0f = full volume
/// </summary>
public float VolumeLeft { get; set; }
/// <summary>
/// Allows adjusting the volume right channel, 1.0f = full volume
/// </summary>
public float VolumeRight { get; set; }
/// <summary>
/// Initializes a new instance of VolumeStereoSampleProvider
/// </summary>
/// <param name="source">Source sample provider, must be stereo</param>
public VolumeStereoSampleProvider(ISampleProvider source)
{
if (source.WaveFormat.Channels != 2)
throw new ArgumentException("Source sample provider must be stereo");
this.source = source;
VolumeLeft = 1.0f;
VolumeRight = 1.0f;
}
/// <summary>
/// WaveFormat
/// </summary>
public WaveFormat WaveFormat => source.WaveFormat;
/// <summary>
/// Reads samples from this sample provider
/// </summary>
/// <param name="buffer">Sample buffer</param>
/// <param name="offset">Offset into sample buffer</param>
/// <param name="sampleCount">Number of samples desired</param>
/// <returns>Number of samples read</returns>
public int Read(float[] buffer, int offset, int sampleCount)
{
int samplesRead = source.Read(buffer, offset, sampleCount);
for (int n = 0; n < sampleCount; n += 2)
{
buffer[offset + n] *= VolumeLeft;
buffer[offset + n + 1] *= VolumeRight;
}
return samplesRead;
}
}
Made with the help of PanningSampleProvider. But for this you have to convert to mono. Also, the reaction to changing Pan - occurs with a slight delay. How can this be avoided? How to work with stereo and just change the volume of its right and left channels? I think this should work much faster.
_audioFile = new AudioFileReader(_fileName) { Volume = _volume };
var mono = new StereoToMonoSampleProvider(_audioFile) { LeftVolume = 1f, RightVolume = 1f };
var panner = new PanningSampleProvider(mono);
_outputDevice.Init(panner);
I am making a tile-based game. I have my "playfield" that is 30*20 Tiles (each is 32x32 pixels). The first Tile that is drawn (at BlockPos (0,0)) is drawn correctly, but the other Tiles are drawn incorrectly. That is, the texture is "smudged". Here is the main class code:
#region Using Statements
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Storage;
using Microsoft.Xna.Framework.GamerServices;
using Texert.Content;
using Texert.Logic;
#endregion
namespace Texert
{
/// <summary>
/// This is the main type for your game
/// </summary>
public class MainGame : Game
{
private FrameCounter _frameCounter = new FrameCounter();
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
public MainGame()
: base()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
/// <summary>
/// Allows the game to perform any initialization it needs to before starting to run.
/// This is where it can query for any required services and load any non-graphic
/// related content. Calling base.Initialize will enumerate through any components
/// and initialize them as well.
/// </summary>
protected override void Initialize()
{
// TODO: Add your initialization logic here
Vars.Textures = new List<Texture2D>();
Vars.Tiles = new List<Tile>();
for (var i = 0; i < 20; i++)
{
for (var k = 0; k < 30; k++)
{
Vars.Tiles.Add(new TileDirt(new BlockPos(k, i)));
}
}
// Set window size
graphics.IsFullScreen = false;
graphics.PreferredBackBufferHeight = Tile.TILE_SIZE*20;
graphics.PreferredBackBufferWidth = Tile.TILE_SIZE*30;
graphics.ApplyChanges();
base.Initialize();
}
/// <summary>
/// LoadContent will be called once per game and is the place to load
/// all of your content.
/// </summary>
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
Vars.Textures.Add(Content.Load<Texture2D>("dirt"));
// TODO: use this.Content to load your game content here
}
/// <summary>
/// UnloadContent will be called once per game and is the place to unload
/// all content.
/// </summary>
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime gameTime)
{
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || Keyboard.GetState().IsKeyDown(Keys.Escape))
Exit();
// TODO: Add your update logic here
// Update FPS
var deltaTime = (float) gameTime.ElapsedGameTime.TotalSeconds;
_frameCounter.Update(deltaTime);
Window.Title = "Texert - " + Math.Floor(_frameCounter.AverageFramesPerSecond) + " FPS";
base.Update(gameTime);
}
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin();
foreach (var tile in Vars.Tiles)
{
spriteBatch.Draw(Vars.Textures[tile.textureIndex],
tile.pos.GetRealPos().GetVector2(),
new Rectangle(tile.pos.GetRealPos().X, tile.pos.GetRealPos().Y,
Tile.TILE_SIZE, Tile.TILE_SIZE),
Color.White); // draw the fucking tile
}
spriteBatch.End();
base.Draw(gameTime);
}
}
}
Here is a screenshot of the game in action. I have no idea why it is drawn this way. How can I fix this?
Edit: Here's the Tile.cs file that contains Pos, BlockPos and Tile!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
namespace Texert.Logic
{
public class Tile
{
public static int TILE_SIZE = 32;
public int textureIndex;
public string tileName;
public BlockPos pos;
public Tile()
{
textureIndex = 0;
tileName = "null";
pos = new BlockPos(0, 0);
}
public Tile(int index, string name, BlockPos pos)
{
textureIndex = index;
tileName = name;
this.pos = pos;
}
}
public class BlockPos
{
public Pos actualPos;
public int X;
public int Y;
public BlockPos(int x, int y)
{
X = x;
Y = y;
}
public Pos GetRealPos()
{
return new Pos(X*Tile.TILE_SIZE, Y*Tile.TILE_SIZE);
}
/// <summary>
/// Returns blockpos of pos, rounded down to nearest multiple of TILE_SIZE
/// </summary>
/// <param name="pos">The position to convert to BlockPos</param>
/// <returns>The BlockPos</returns>
static BlockPos GetBlockPosFromPos(Pos pos)
{
return new BlockPos(pos.X % Tile.TILE_SIZE, pos.Y % Tile.TILE_SIZE);
}
}
public class Pos
{
public int X;
public int Y;
public Pos(int x, int y)
{
X = x;
Y = y;
}
public Vector2 GetVector2()
{
return new Vector2(X, Y);
}
}
}
I'm currently working on an exercise for my c# class. I am having some trouble with one particular part and would really appreciate some help.
I am working on an exercise in which we are given an incomplete project file. The project was to create a board game which lets up to six people play (fairly simple in theory). The part I am currently stuck on is showing the player "tokens" on the board.
This is what the final board is meant to look like:
As you can see, the on "0" (start square) there are 6 circles (or tokens) and on the right side there is a datagrid view with columns showing relevant information (colour, name, money, winner).
This is what I have been able to do so far:
As you see, I have been able to show the player name, and money. Though I cannot get the colour to show up in the start square or the data grid view. Instead, I get this:
The two relevant classes are as follows:
Player.CS hold the player object code (constructors etc) (sorry for the wall of text in advance, I am unsure which parts to leave out)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Diagnostics;
namespace SharedGameClasses {
/// <summary>
/// Models a player who is currently located on a particular square
/// with a certain amount of money.
/// </summary>
public class Player {
private const int INITIAL_AMOUNT = 100;
// name of the player
private string name;
public string Name {
get {
return name;
}
set {
name = value;
}
}
// amount of money owned by player
private int money;
public int Money {
get {
return money;
}
set {
money = value;
}
}
// current square that player is on
private Square location;
public Square Location {
get {
return location;
}
set {
location = value;
}
}
// whether the player is a winner, in the current game.
private bool winner;
public bool Winner {
get {
return winner;
}
set {
winner = value;
}
}
// PlayerTokenColour and PlayerTokenImage provide colours for the players' tokens (or "pieces").
private Brush playerTokenColour;
public Brush PlayerTokenColour {
get {
return playerTokenColour;
}
set {
playerTokenColour = value;
playerTokenImage = new Bitmap(1, 1);
using (Graphics g = Graphics.FromImage(PlayerTokenImage)) {
g.FillRectangle(playerTokenColour, 0, 0, 1, 1);
}
}
}
private Image playerTokenImage;
public Image PlayerTokenImage {
get {
return playerTokenImage;
}
}
/// <summary>
/// Parameterless constructor.
/// Do not want the generic default constructor to be used
/// as there is no way to set the player's name.
/// This replaces the compiler's generic default constructor.
/// Pre: none
/// Post: ALWAYS throws an ArgumentException.
/// </summary>
/// <remarks>NOT TO BE USED!</remarks>
public Player() {
throw new ArgumentException("Parameterless constructor invalid.");
} // end Player constructor
/// <summary>
/// Constructor with initialising parameters.
/// Pre: name to be used for this player.
/// Post: this player object has all attributes initialised
/// </summary>
/// <param name="name">Name for this player</param>
public Player(String name, Square initialLocation, Brush playerToken){
Name = name;
location = initialLocation;
Money = INITIAL_AMOUNT;
PlayerTokenColour = playerToken;
//######################### Code needs to be added here ##########################################
} // end Player constructor
/// <summary>
/// Rolls the two dice to determine
/// the number of squares to move forward; and
/// moves the player's location along the board; and
/// obtains the effect of landing on their final square.
/// Pre: dice are initialised
/// Post: the player is moved along the board and the effect
/// of the location the player landed on is applied.
/// </summary>
/// <param name="d1">first die</param>
/// <param name="d2">second die</param>
public void Play(Die d1, Die d2){
var roll1 = d1.Roll();
var roll2 = d2.Roll();
int numofSquares = roll1 + roll2;
Move(numofSquares);
//######################### Code needs to be added here ##########################################
} // end Play.
/// <summary>
/// Moves player the required number of squares forward
/// Pre: the number of squares to move forward
/// Post: the player is moved along the board.
/// NOTE: Refer to Square.cs regarding the NextSquare property.
/// </summary>
/// <param name="numberOfSquares">the number of squares to move</param>
private void Move(int numberOfSquares) {
//######################### Code needs to be added here ##########################################3
} //end Move
/// <summary>
/// Increments the player's money by amount
/// Pre: amount > 0
/// Post: the player's money amount is increased.
/// </summary>
/// <param name="amount">increment amount</param>
public void Credit(int amount) {
Money = Money + amount;
} //end Credit
/// <summary>
/// Decreases the player's money by amount if
/// the player can afford it; otherwise,
/// sets the player's money to 0.
/// Pre: amount > 0
/// Post: player's money is decremented by amount if possible
/// but final amount is not below zero
/// </summary>
/// <param name="amount">decrement amount</param>
public void Debit(int amount){
const int loseamount = 25;
if (Money >= 25){
Money = Money - loseamount;
} else if (Money < 25){
Money = 0;
}
//######################### Code needs to be added here ##########################################3
} //end Debit
} //end class Player
}
And HareandTortoiseGame.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Drawing;
using System.ComponentModel; // for BindingList.
namespace SharedGameClasses {
/// <summary>
/// Plays a game called Hare and the Tortoise
/// </summary>
public static class HareAndTortoiseGame {
// Minimum and maximum players per game
private const int MIN_PLAYERS = 2;
public const int MAX_PLAYERS = 6;
// The dice
private static Die die1 = new Die(), die2 = new Die();
// A BindingList is like an array that can grow and shrink.
//
// Using a BindingList will make it easier to implement the GUI with a DataGridView
private static BindingList<Player> players = new BindingList<Player>();
public static BindingList<Player> Players {
get {
return players;
}
}
private static int numberOfPlayers = 6; // The value 6 is purely to avoid compiler errors.
public static int NumberOfPlayers {
get {
return numberOfPlayers;
}
set {
numberOfPlayers = value;
}
}
// Is the current game finished?
private static bool finished = false;
public static bool Finished {
get {
return finished;
}
}
/// Some default player names.
///
/// These are purely for testing purposes and when initialising the players at the start
///
/// These values are intended to be read-only. I.e. the program code should never update this array.
private static string[] defaultNames = { "One", "Two", "Three", "Four", "Five", "Six" };
// Some colours for the players' tokens (or "pieces").
private static Brush[] playerTokenColours = new Brush[MAX_PLAYERS] { Brushes.Black, Brushes.Red,
Brushes.Gold, Brushes.GreenYellow,
Brushes.Fuchsia, Brushes.White };
/// <summary>
/// Initialises each of the players and adds them to the players BindingList.
/// This method is called only once, when the game first startsfrom HareAndTortoiseForm.
///
/// Pre: none.
/// Post: all the game's players are initialised.
/// </summary>
public static void InitialiseAllThePlayers(){
//Player Playerone = new Player(defaultNames[1], Board.Squares[0]);
int i = 0;
while (i < NumberOfPlayers){
players.Add(new Player(defaultNames[i], Board.Squares[0], playerTokenColours[i]));
i++;
}
//##################### Code needs to be added here. ############################################################
} // end InitialiseAllThePlayers
/// <summary>
/// Puts all the players on the Start square.
/// Pre: none.
/// Post: the game is reset as though it is being played for the first time.
/// </summary>
public static void SetPlayersAtTheStart() {
//##################### Code needs to be added here. ############################################################
} // end SetPlayersAtTheStart
public static void PlayOneRound(){
InitialiseAllThePlayers();
}
} //end class HareAndTortoiseGame
}
Any help/tips will be appreciated, thanks! If you need any more information let me know
EDIT: Additionally, I believe these methods from another class (HareandTortoiseForm.cs) are relevant
/// <summary>
/// Constructor with initialising parameters.
/// Pre: none.
/// Post: the form is initialised, ready for the game to start.
/// </summary>
public HareAndTortoiseForm() {
InitializeComponent();
HareAndTortoiseGame.NumberOfPlayers = HareAndTortoiseGame.MAX_PLAYERS; // Max players, by default.
HareAndTortoiseGame.InitialiseAllThePlayers();
Board.SetUpBoard();
SetupTheGui();
ResetGame();
}
And also ResetGame() which is where I think I am going wrong (i think this where i need to add code)
/// <summary>
/// Resets the game, including putting all the players on the Start square.
/// This requires updating what is displayed in the GUI,
/// as well as resetting the attrtibutes of HareAndTortoiseGame .
/// This method is used by both the Reset button and
/// when a new value is chosen in the Number of Players ComboBox.
/// Pre: none.
/// Post: the form displays the game in the same state as when the program first starts
/// (except that any user names that the player has entered are not reset).
/// </summary>
private void ResetGame() {
// ########################### Code needs to be written ###############################################
}
EDIT 2:
/// <summary>
/// Tells you which SquareControl object is associated with a given square number.
/// Pre: a valid squareNumber is specified; and
/// the boardTableLayoutPanel is properly constructed.
/// Post: the SquareControl object associated with the square number is returned.
/// </summary>
/// <param name="squareNumber">The square number.</param>
/// <returns>Returns the SquareControl object associated with the square number.</returns>
private SquareControl SquareControlAt(int squareNumber) {
int rowNumber;
int columnNumber;
MapSquareNumToScreenRowAndColumn(squareNumber, out rowNumber, out columnNumber);
// Uncomment the following line once you've added the boardTableLayoutPanel to your form.
return (SquareControl) boardTableLayoutPanel.GetControlFromPosition(columnNumber, rowNumber);
// Delete the following line once you've added the boardTableLayoutPanel to your form.
// return null;
} //end SquareControlAt
I can't help you on placing the tokens on DataGridView, but I would rather change the whole board into a TableLayoutPanel. That way you can easily handle each cell separately, and tokens could be just some controls, which are set visible or not.
The Colors should be shown by simply assigning them to the relevant Cells of the DataGridView:
DGV.Rows[playerIndex].Cells[0].Style.BackColor = playerColor;
The Tokens probably ought to be drawn
either on a Bitmap you display in a PictureBox
or directly on a Panel in its Paint event.
Both options are OK. Does this token area change during the game?
Is there an indication in the project that can give you a hint..?
In both cases you will need the Graphics.FillEllipse method.
The player box is continuing through walls in an undesired fashion, I have tried making it so that the player moves in 0.1f(u) increments at a time, but this severely drops the performance of the game. Is there any way I can detect if the player is hitting a wall, what side they hit it on and how can I prevent them from clipping into the wall?
Here is the code that I am running (this is minimalistic of course)
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
namespace Platformer
{
public class Player
{
double terminalVelocity;
//AnimatedTexture texture;
public Texture2D texture;
public Vector2 Position, Velocity;
public Rectangle boundingBox;
public Player(Texture2D tex, Vector2 pos, Vector2 vel, Rectangle bb)
{
texture = tex;
Position = pos;
Velocity = vel;
boundingBox = bb;
terminalVelocity = Math.Sqrt((2*bb.Width*bb.Height*Game1.gravity)/-9.8*2);
}
public void updateBoundingBoxes()
{
boundingBox.X = (int)Position.X;
boundingBox.Y = (int)Position.Y;
}
public void onUpdate()
{
updateBoundingBoxes();
Position.X += Velocity.X;
Position.Y += Velocity.Y;
//Velocity = Vector2.Zero;
Velocity.Y += Game1.gravity / 60;
Velocity.X /= 1.2f;
}
public void Draw(SpriteBatch sb)
{
updateBoundingBoxes();
sb.Begin();
sb.Draw(texture,boundingBox,GameLighting.currentColour());
sb.End();
}
}
public enum GameLightingState
{
Red, Dark, Orange, Blue, White
}
public class Platform : Object
{
Texture2D text;
public Rectangle rect;
public Platform(Texture2D t, Vector2 p, int sizeX, int sizeY)
{
text = t;
rect = new Rectangle((int)p.X, (int)p.Y, sizeX, sizeY);
}
public void onPlayerCollision(Player p)
{
p.Velocity.X = -p.Velocity.X / 2;
p.Velocity.Y = -p.Velocity.Y / 2;
}
public void Draw(SpriteBatch sb)
{
sb.Begin();
sb.Draw(text, rect, GameLighting.currentColour());
sb.End();
}
public void onUpdate()
{
}
}
public class GameLighting
{
public static Color currentColour()
{
return eToColour(Game1.currentLightingState);
}
public static Color eToColour(GameLightingState gls)
{
switch (gls)
{
case(GameLightingState.Red):
return Color.Red;
case (GameLightingState.Blue):
return Color.Blue;
case (GameLightingState.Orange):
return Color.Orange;
case (GameLightingState.Dark):
return Color.DarkGray;
case (GameLightingState.White):
return Color.White;
}
return Color.White;
}
}
/// <summary>
/// This is the main type for your game
/// </summary>
public class Game1 : Microsoft.Xna.Framework.Game
{
public static float gravity = 9.80665f;
public static GameLightingState currentLightingState;
GraphicsDeviceManager graphics;
SpriteBatch spriteBatch;
List<Platform> platforms;
List<Player> players;
int controlledPlayerIndex = 0;
public Game1()
{
graphics = new GraphicsDeviceManager(this);
Content.RootDirectory = "Content";
}
/// <summary>
/// Allows the game to perform any initialization it needs to before starting to run.
/// This is where it can query for any required services and load any non-graphic
/// related content. Calling base.Initialize will enumerate through any components
/// and initialize them as well.
/// </summary>
protected override void Initialize()
{
// TODO: Add your initialization logic here
currentLightingState = GameLightingState.White;
platforms = new List<Platform>();
players = new List<Player>();
players.Add(new Player(this.Content.Load<Texture2D>("Images/dirt"), new Vector2(300,0), new Vector2(0,0), new Rectangle(300,0,20,20)));
platforms.Add(new Platform(this.Content.Load<Texture2D>("Images/dirt"),new Vector2(300,450),200,20));
platforms.Add(new Platform(this.Content.Load<Texture2D>("Images/dirt"), new Vector2(20,20), 20, 200));
base.Initialize();
}
/// <summary>
/// LoadContent will be called once per game and is the place to load
/// all of your content.
/// </summary>
protected override void LoadContent()
{
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(GraphicsDevice);
// TODO: use this.Content to load your game content here
}
/// <summary>
/// UnloadContent will be called once per game and is the place to unload
/// all content.
/// </summary>
protected override void UnloadContent()
{
// TODO: Unload any non ContentManager content here
}
/// <summary>
/// Allows the game to run logic such as updating the world,
/// checking for collisions, gathering input, and playing audio.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Update(GameTime gameTime)
{
// Allows the game to exit
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
this.Exit();
foreach (Player p in players)
{
Boolean intersects = false;
Rectangle tempRectangle = new Rectangle((int)(p.Position.X + p.Velocity.X),(int) (p.Position.Y + p.Velocity.Y), p.boundingBox.Width, p.boundingBox.Height);
foreach (Platform pl in platforms)
{
intersects = intersects || tempRectangle.Intersects(pl.rect);
}
if (!intersects)
{
p.onUpdate();
}
}
if (Keyboard.GetState().IsKeyDown(Keys.Space))
{
players[controlledPlayerIndex].Velocity.Y -= 0.75f;
}
if (Keyboard.GetState().IsKeyDown(Keys.A))
{
players[controlledPlayerIndex].Velocity.X -= 0.75f;
}
if (Keyboard.GetState().IsKeyDown(Keys.D))
{
players[controlledPlayerIndex].Velocity.X += 0.75f;
}
// TODO: Add your update logic here
base.Update(gameTime);
}
/// <summary>
/// This is called when the game should draw itself.
/// </summary>
/// <param name="gameTime">Provides a snapshot of timing values.</param>
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.CornflowerBlue);
// TODO: Add your drawing code here
foreach (Platform p in platforms)
{
p.Draw(spriteBatch);
}
foreach (Player p in players)
{
p.Draw(spriteBatch);
}
base.Draw(gameTime);
}
}
}
*Updated Source Code based on first comments
One note about this code, you need to run it in XNA and use an icon called dirt.png in a folder called Images, it doesn't matter what the picture looks like, you just need it to fully understand what is happening
Had a similar problem recently myself. Here is what I did.
//variables needed
public bool follow = true;
public Vector2D startpoint;
//puts the startpoint value equal
//to the inital location of the player
public Player()
{
startpoint.X = rectangle.X;
startpoint.Y = rectangle.Y;
}
//if any of the collision tests fail
if(collision occurs)
{
collision();
}
//else update startpoint to new valid location
else
{
startpoint.X = rectangle.X;
startpoint.Y = rectangle.Y;
follow = true;
}
if(follow == true)
{
//movement commands occur
}
else
{
follow = true;
}
//if a condition fails set player
//too last valid location
public void collision()
{
rectangle.X = startpoint.X;
rectangle.Y = startpoint.Y;
follow = false;
}
This worked well enough for me hope it helps.