Thursday 29 October 2015

Mac won't power up

Problem

Issue with powering on MacBook Pro Retina.
Tried a hard reset (hold power button down for 10 seconds).
This did not solve the issue.
When the power button was pressed, the Apple logo light on the laptop was on.
Toggling Caps-Lock did not power on or off the light in the Caps-Lock key.
There was some heat coming from the laptop when touching the bottom of if.

Solution

The solution was to reset the NVRAM.
To do this, do a hard poweroff (hold down the power button for about 10 seconds). Verify that the Apple logo on the laptop lid is not on.
Now press the power button.
Immediately after pressing the power button hold down the following keys. Command, Option, P and R. You will then hear the start up sound play.
Keep holding the keys down until the startup sound plays again, or till the login screen is displayed.


More details about this can be found on the Apple website https://support.apple.com/en-us/HT204063

Sunday 19 July 2015

Unity 3D - Red Lines in Scene View when importing UI Prefab

I had an issue importing a UI prefab using Unity 5. When I imported it, there were a lot a red lines over the scene. The prefab was not showing up in the scene view or game view. It appears as if the UI prefab is corrupted or is invalid.


The problem here is that the UI prefab was not imported into the UI canvas. 
This is fixed by:
  1. Removing the prefab from the scene
  2. Add a UI Canvas to the scene
  3. Add the prefab to the Canvas

Saturday 20 June 2015

Auto Position Game Objects in Random Positions in Unity

I was working on a 2D game in Unity and I wanted a lot of game objects placed randomly in the level but I didn't want to spend a lot of time laying out each individual game object. I wanted to generate a lot of clouds in a level but without the hassle of manually laying them all out.

I achieved this by creating and attaching a script that would automatically layout the position of all the game objects in the level.


This is what an example of what the level looks like.

I created a C# script called GameObjectGenerator.cs.

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

/***
 * Programatically generate a list of GameObjects at random positions 
 * between xOffset to xLength, yOffset to yLength and zOffset to zLength.
 */
public class GameObjectGenerator : MonoBehaviour {
 
 /***
  * The maximum number of objects to get generated
  */
 public int maxObjects = 10;

 /***
  * The type of game object to generate
  */
 public GameObject generatedObjectType;

 /***
  * The x-axis offset to position the game objects
  */
 public float xOffset = 0f;
 /***
  * The y-axis offset to position the game objects
  */
 public float yOffset = 0f;
 /***
  * The z-axis offset to position the game objects
  */
 public float zOffset = 0f;

 /***
  * The length of the level on the x-axis
  */
 public float xLength = 10f;
 /***
  * The length of the level on the y-axis
  */
 public float yLength = 10f;
 /***
  * The length of the level on the z-axis
  */
 public float zLength = 10f;

 /***
  * Internal list of all the generated game objects. 
  */
 private List gameObjects;

 void Start () {
  gameObjects = new List();
  generateGameObjects ();
 }
 
 public void generateGameObjects() {
  for (int i = 0; i < maxObjects; i++) {
   //get a random value between the offset and the length
   float xPos = Mathf.Clamp(xOffset + Random.value * xLength, xOffset, xLength);
   float yPos = Mathf.Clamp(yOffset + Random.value * yLength, yOffset, yLength);
   float zPos = Mathf.Clamp(zOffset + Random.value * zLength, zOffset, zLength);

   Vector3 position = new Vector3 (xPos, yPos, zPos);

   GameObject gameObject = (GameObject)Instantiate (generatedObjectType, position, Quaternion.identity);
   gameObjects.Add(gameObject);
  }
 }
}



First thing to do to get this working, is to create an empty game object and added it to the scene. I called it CloudGenerator. Attach the GameObjectGenerator script to the CloudGenerator. Once the script is attached, you can see all the parameters that can be changed for the script.

 
Description
Max Objects is the number of Game Objects that are going to be instantiated in the level.
Generated Object Type is the game object that is going to be generated throughout the level. In my case, it was a cloud sprite Game Object.
X Offset is the position on the X-Axis where the game objects will begin to get placed.
Y Offset is the position on the Y-Axis where the game objects will begin to get placed.
Z Offset is the position on the Z-Axis where the game objects will begin to get placed.
X Length is the length of the level in the X-Axis.
Y Length is the length of the level in the Y-Axis.
Z Length is the length of the level in the Z-Axis.

So from the example above, there will be 100 Cloud objects created in random positions from (-42, 4, -5) to (300, 7, 50).

This also brings parallax to the game since objects are created at random values on the Z-Axis. If you don't want parallax in your game, just set Z Offset and Z Length to 0.

Monday 4 May 2015

Simple Photo Cropper in Java

I recently scanned in an old box of photos for my family. The scanned image had a few photos surrounded by a lot of whitespace.

I didn't want to have to open each photo manually, crop it, choose a file name, repeat. I wanted to have some of this process automated.

I wrote this simple program to make this process easier.

First you choose a directory to work from.
Once this is done, the first image from the directory is loaded.
You can then click and drag a box around an image the image you want to crop.
Once you are happy with the image selection, click the Crop button. This will save the selection to a file.
The file name is the time since epoch in milliseconds.
The file format is png.
The files are saved in a "cropped" directory. This is in the same directory as the original images.

You can then repeat this multiple times per image.
Once you want to work on the next image, you can click the next button for the next image or the prev button for the previous image.
There is a slider allows that allows to zoom in and zoom out of the image.




package photocropper;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;

/***
 * This is the main class of the application. It constructs the UI and 
 * registers event listeners,
 * @author andrew
 *
 */
public class PhotoCropper {

	private ArrayList photos;
	private int currentImageIndex = -1;
	private String directory;

	public PhotoCropper() throws Exception {
		frame = new JFrame();
		panel = new ImagePanel();

		while((directory = getPhotoDirectory()) == null) {
			System.err.println("This application needs to have a photo directory. Exiting...");
			System.exit(1);
		}	

		String[] files = Utils.getFiles(directory);

		ArrayList fileList = Utils.toArrayList(files);
		Utils.filterNonImages(fileList);
		photos = fileList;

		panel.setOutputDirectory(directory + File.separator + "cropped");

		CropMouseListener cropListener = new CropMouseListener(panel);
		panel.addMouseListener(cropListener);
		panel.addMouseMotionListener(cropListener);

		if(photos.size() > 0) {
			BufferedImage img = Utils.loadImage(directory, photos.get(0));
			frame.setTitle(photos.get(0)+ "\t" + 0 + "/" + photos.size());
			currentImageIndex = 0;
			panel.changeImage(img);
		}

		JPanel controlPanel = new JPanel();

		btnNext = new JButton("Next");
		btnPrev = new JButton("Prev");
		btnCrop = new JButton("Crop");

		sliderZoom = new JSlider(1, 100);
		sliderZoom.setValue(100);
		setUpListeners();
		controlPanel.add(btnPrev);
		controlPanel.add(sliderZoom);
		controlPanel.add(btnNext);
		controlPanel.add(btnCrop);

		//set resizeable to false so the resize will not interfere 
		//when interacting with images close to the border
		frame.setResizable(false);

		frame.add(controlPanel, BorderLayout.SOUTH);

		frame.add(panel, BorderLayout.CENTER);

		frame.setSize(new Dimension(1400,800));
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.setVisible(true);
	}

	/***
	 * 
	 * @return
	 */
	public String getPhotoDirectory() {
		JFileChooser fileChooser = new JFileChooser();
		String dir = null;
		fileChooser.setDialogTitle("Select Photo Directory");

		fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);

		int returnVal = fileChooser.showOpenDialog(frame);
		if (returnVal == JFileChooser.APPROVE_OPTION) {
			dir = fileChooser.getSelectedFile().getAbsolutePath();
		}

		return dir;
	}

	public void setUpListeners() {
		btnNext.addActionListener((ActionEvent evt) -> {
			if((currentImageIndex + 1) >= photos.size()) {
				currentImageIndex = 0;
			}
			else {
				currentImageIndex++;
			}

			try {
				BufferedImage prevImage = Utils.loadImage(directory, photos.get(currentImageIndex));
				frame.setTitle(photos.get(currentImageIndex) + "\t" + currentImageIndex + "/" + photos.size());
				panel.changeImage(prevImage);
				frame.repaint();
			}
			catch(IOException e) {
				System.err.println("ERROR: could not load " + photos.get(currentImageIndex));
				e.printStackTrace();
			}
		});

		btnPrev.addActionListener((ActionEvent evt) -> {
			if((currentImageIndex - 1) < 0) {
				currentImageIndex = photos.size() -1;
			}
			else {
				currentImageIndex--;
			}

			try {
				BufferedImage prevImage = Utils.loadImage(directory, photos.get(currentImageIndex));
				frame.setTitle(photos.get(currentImageIndex) + "\t" + currentImageIndex + "/" + photos.size());
				panel.changeImage(prevImage);
				frame.repaint();
			}
			catch(IOException e) {
				System.err.println("ERROR: could not load " + photos.get(currentImageIndex));
				e.printStackTrace();
			}
		});

		btnCrop.addActionListener((ActionEvent evt) -> {
			try{
				panel.crop();
			}
			catch(Exception e) {
				JOptionPane.showMessageDialog(frame,
					    "Could not crop image. Bad image selection",
					    "Crop Error",
					    JOptionPane.ERROR_MESSAGE);
			}
		});

		sliderZoom.addChangeListener((ChangeEvent e) -> {
			double value = sliderZoom.getValue();
			double zoom = value / 100.0;
			panel.setZoom(zoom);
		});
	}

	public static void main(String[] args) {
		try {
			new PhotoCropper(); 			
		}
		catch(Exception e) {
			e.printStackTrace();
		}
	}

	private JButton btnNext;
	private JButton btnPrev;
	private JButton btnCrop;
	private JSlider sliderZoom;
	private ImagePanel panel;
	private JFrame frame;
}

package photocropper;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.swing.JPanel;

/***
 * This component displays the image.
 * It also has functionality to crop the image.
 *  
 * @author andrew
 *
 */
public class ImagePanel extends JPanel {

	private double zoom;
	private double scale;

	/**
	 * 
	 */
	private static final long serialVersionUID = -2179167993341600319L;

	private BufferedImage displayImage;
	private CropBox cropBox;
	private String dir;

	public ImagePanel() {
		cropBox = new CropBox(0, 0, 0, 0);
		zoom = 1.0;
		scale = 1.0;
		dir = "";
	}

	/***
	 * Change the image that is displayed in the panel
	 * @param image
	 */
	public void changeImage(BufferedImage image) {
		displayImage = image;
	}

	/***
	 * Set the first coordinate of the crop box.
	 * This is the top corner of the crop box
	 * @param x
	 * @param y
	 */
	public void setFirstCoord(int x, int y) {
		cropBox.x = x;
		cropBox.y = y;
		this.repaint();
	}

	/***
	 * Set the second coordinate of the crop box.
	 * This is the bottom corner of the crop box.
	 * @param x
	 * @param y
	 */
	public void setSecondCoord(int x, int y) {
		cropBox.width = x - cropBox.x;
		cropBox.height = y - cropBox.y;
		this.repaint();
	}

	/***
	 * Set the size of the crop box
	 * @param width
	 * @param height
	 */
	public void setCropBoxSize(int width, int height) {
		cropBox.width = width;
		cropBox.height = height;
		this.repaint();
	}
	
	/***
	 * Set the zoom. This allows zooming in and out of the display image
	 * @param zoom
	 */
	public void setZoom(double zoom) {
		this.zoom = zoom;
		this.scale = 1 / zoom;
		this.repaint();
	}

	/***
	 * Draw the component. 
	 * Draw the image then superimpose the cropbox on top of it.
	 */
	@Override
	public void paintComponent(Graphics graphics) {
		Graphics2D g = (Graphics2D) graphics;

		if(displayImage != null) {
			AffineTransform at = new AffineTransform();
			at.setToScale(zoom, zoom);
			g.drawImage(displayImage, at, this);
		}
		else {
			g.drawString("Image Not Available", (int)(getWidth()/2.5), getHeight()/2);
		}

		if(cropBox != null) {
			Color oldColor = g.getColor();
			g.setColor(cropBox.color);
			g.drawRect(cropBox.x, cropBox.y, cropBox.width, cropBox.height);

			g.setColor(oldColor);
		}
	}

	/***
	 * Crop the image selection and save to file
	 * @throws Exception
	 */
	public void crop() throws Exception {
		try{
			if(displayImage == null) {
				System.err.println("ERROR: No image to crop");
				return;
			}

			File outFile = new File(dir +   File.separator + System.currentTimeMillis() + ".png");
			
			//get the crop box coords with respect to the zoom property
			int x = (int)(cropBox.x * scale);
			int y = (int)(cropBox.y * scale);
			int width = (int)(cropBox.width *scale);
			int height = (int)(cropBox.height * scale);

			//reset for bad input
			if(x < 0) x = 0;
			if(y < 0) y = 0;
			if(width < 0) width = 0;
			if(height < 0) height = 0;

			//correct the width if it is out of bounds
			if((x + width) > displayImage.getWidth()) {
				width = displayImage.getWidth() - x;				
			}

			//correct the height if it is out of bounds
			if((y + height) > displayImage.getHeight()) {
				height = displayImage.getHeight() - y;
			}

			//throw exception if selection is not in the image at all
			if(x > displayImage.getWidth()) {
				throw new Exception("Image selection is out of bounds");
			}
			if(y > displayImage.getHeight()) {
				throw new Exception("Image selection is out of bounds");
			}

			//crop the sub image to save
			BufferedImage img = displayImage.getSubimage(x, y, 
					width, height);

			//check for the destination directory, create it if it doesn't exist
			File outputDir = new File(dir);
			if(!outputDir.isDirectory()) {
				System.out.println("Output directory does not exist, creating: "
						+ outputDir.getAbsolutePath());
				outputDir.mkdir();
			}			

			ImageIO.write(img, "png", outFile);
		}
		catch (IOException e) {
			System.err.println("Could not save to file. Check directory permissions");
		}
	}

	/***
	 * Set the save directory
	 * @param dir
	 */
	public void setOutputDirectory(String dir) {
		this.dir = dir;
	}

	/***
	 * CropBox contains the properties of the cropping box
	 * @author andrew
	 */
	private class CropBox {
		private int x, y, width, height;
		private Color color;

		public CropBox(int x, int y, int width, int height) {
			this.x = x;
			this.y = y;
			this.width = width;
			this.height = height;
			this.color = Color.RED;
		}
	}
}


package photocropper;

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

/***
 * This listens to mouse events and sets the coordinates 
 * of the bounding box of the crop. 
 * 
 * @author andrew
 */
public class CropMouseListener implements MouseListener, MouseMotionListener {
	private ImagePanel panel;
	
	public CropMouseListener(ImagePanel panel) {
		this.panel = panel;
	}
	
	@Override
	public void mouseReleased(MouseEvent e) {
		if(panel != null) {
			panel.setSecondCoord(e.getX(), e.getY());
		}
	}
	
	/***
	 * If right click, zero out any existing box
	 */
	@Override
	public void mousePressed(MouseEvent e) {
		if(panel != null) {
			panel.setCropBoxSize(0, 0);
			
			if(e.getButton() == MouseEvent.BUTTON3) {
				panel.setFirstCoord(0, 0);
				panel.setSecondCoord(e.getX(), e.getY());
				return;
			}
			
			panel.setFirstCoord(e.getX(), e.getY());
		}
	}

	@Override
	public void mouseDragged(MouseEvent e) {
		//TODO - this only works for positive width and height
		//rework this so that it accommodates for both
		//negative width and height
		panel.setSecondCoord(e.getX(), e.getY());	
	}
	
	@Override
	public void mouseExited(MouseEvent e) {
		//Not interested in this event
	}
	
	@Override
	public void mouseEntered(MouseEvent e) {
		//Not interested in this event
	}
	
	@Override
	public void mouseClicked(MouseEvent e) {
		//Not interested in this event
	}
	
	@Override
	public void mouseMoved(MouseEvent e) {
		//Not interested in this event
	}
}


package photocropper;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

import javax.imageio.ImageIO;

/***
 * Some utilities used in the photo cropper
 * @author andrew
 *
 */
public class Utils {
	
	/***
	 * Load an image from file
	 * @param directory the directory path to the image
	 * @param filename the filename of the image
	 * @return the image in memory
	 * @throws IOException Thrown if cannot open the image
	 */
	public static BufferedImage loadImage(String directory, String filename)
			throws IOException {
		BufferedImage image = null;
		String dir = "";
		
		if(!directory.endsWith("/")) {
			dir = directory + "/";
		}
		else {
			dir = directory;
		}
		
		File file = new File(dir + filename);
		try{
			image = ImageIO.read(file);
		}
		catch(IOException e) {
			System.err.println("cannot open file " + file.getAbsolutePath() + e.getLocalizedMessage());
			throw e;
		}
		return image;
	}
	
	/***
	 * Get the names of all the files in a directory
	 * @param dirName The path of the directory
	 * @return a list of the files in a directory
	 */
	public static String[] getFiles(String dirName) {
		File dir = new File(dirName);
		
		if(dir.isDirectory()) {
			String[] files = dir.list();
			
			return files;
		}
		return null;
	}
	
	/***
	 * Helper method to convert an array to an arraylist
	 * @param array The array to convert
	 * @return returns the contents of the array in an arraylist format
	 */
	public static ArrayList toArrayList(String[] array) {
		ArrayList list = new ArrayList<>();
		
		for(String item: array) {
			list.add(item);
		}
		return list;
	}
	
	/***
	 * Get the file extension from a filename. 
	 * This works by getting the last occurrence of the dot
	 * and extracting the characters after it.
	 * If test.txt is passed in, the string txt is returned.
	 * @param file The name of the file
	 * @return the file extension 
	 */
	public static String getExtension(String file) {
		String extension = null;
		
		if(file == null) {
			return null;
		}
		
		int indexOfDot = file.lastIndexOf(".");
		
		if(indexOfDot != -1) {
			extension = file.substring(indexOfDot + 1);
		}
		
		return extension;
	}
	
	/***
	 * Loop through a file list and remove any non-images.
	 * @param fileList
	 */
	public static void filterNonImages(ArrayList fileList) {
		ArrayList fileToBeRemoved = new ArrayList<>();
		
		//store list of which files need to be filtered out
		for(String file: fileList) {
			if(!isImage(file)) {
				fileToBeRemoved.add(file);
			}
		}
		
		//filter out files from the original list
		for(String file: fileToBeRemoved) {
			fileList.remove(file);			
		}
	}
	
	/***
	 * Check if the file is an image.
	 * This is done based on the file extension
	 * @param filename
	 * @return
	 */
	public static boolean isImage(String filename) {
		String[] imageExtensions = {"png", "jpg"};
		
		String extension = getExtension(filename);
		
		if(extension == null) {
			return false;
		}
		
		for(String ext: imageExtensions) {
			if(ext.equals(extension.toLowerCase())) {
				return true;
			}
		}
		
		return false;
	}
}

Tuesday 13 January 2015

Deploy Unity 3D Project to Samsung Galaxy Tab

Install the drivers

Install the Samsung Kies software
It can be downloaded from http://www.samsung.com/ie/support/usefulsoftware/KIES/

Enable Developer Options on the Tablet

On the tablet
Go to Settings -> General -> Left hand panel, scroll down to About device.
In the About device main panel, scroll down to Build number and tap it 7 times.
Quit the settings application.
Go to Settings -> General -> Left hand panel, scroll down to Developer options.
Tab the Enable USB debugging checkbox

Configure the Build Settings

In Unity go to File -> Build Settings...
In the Platform panel click Android and then click the Switch Platform button
Click on Add Current to add your scene to the android package. If you have multiple scenes in the game, make sure to add them all and make sure that they are in the correct order

Build the Game and Deploy to the Tablet

Then in Unity do File -> Build and Run

The first time you perform a build and run, you should get a notification on the tablet to allow the application to get pushed to the tablet. Allow this. Pushing the game to the device might fail, if it does click Build and Run again in Unity. It should start the game on the tablet.